feat(deploy): installeur Inno Setup pour déploiement professionnel

- Lea.iss : script Inno Setup 6 (enrollment 2 pages, licence, machine_id)
- build_installer.sh : staging + ISCC (compatible Wine sur Linux)
- uninstall_lea.ps1 : kill PID + cleanup + notif serveur
- configure_embed.ps1 : Python 3.12 embedded optionnel
- config_template.txt : modèle pour installation silencieuse
- LICENSE.txt : CGU AI Act Art. 50
- README.md : doc build, signing, déploiement silencieux

Paramètres d'installation silencieuse :
  Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=enroll.txt /LOG=install.log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-14 16:48:48 +02:00
parent bb4ed2a75d
commit 376e4a88b3
7 changed files with 1300 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
============================================================
Lea - Conditions Generales d'Utilisation
============================================================
Version 1.0 — Avril 2026
Editeur : AIVANOV
1. OBJET
--------
Lea est un logiciel d'assistance intelligente destine a automatiser
des taches repetitives sur poste de travail Windows, pour le compte
de son employeur (AIVANOV et ses clients autorises).
2. NATURE DES DONNEES COLLECTEES
--------------------------------
Lors de son utilisation, Lea capture :
- Des captures d'ecran du poste de travail
- Les evenements clavier et souris
- Les metadonnees systeme (nom de la machine, processus actifs)
Les donnees sensibles (mots de passe, numeros de securite sociale,
informations de cartes bancaires) sont automatiquement floutees
avant transmission au serveur, sauf desactivation explicite par
l'administrateur (parametre RPA_BLUR_SENSITIVE=false).
3. TRANSMISSION ET STOCKAGE
---------------------------
Les donnees sont transmises via HTTPS chiffre a un serveur central
gere par AIVANOV. Elles sont conservees 180 jours minimum pour des
raisons de conformite, puis purgees automatiquement.
4. SYSTEME D'IA (AI ACT - ARTICLE 50)
-------------------------------------
Lea utilise des modeles d'intelligence artificielle pour comprendre
et automatiser les taches. Conformement a l'Article 50 du Reglement
europeen sur l'Intelligence Artificielle, l'utilisateur est informe
qu'il interagit avec un systeme d'IA.
5. CONTROLE PAR L'UTILISATEUR
-----------------------------
L'utilisateur peut a tout moment :
- Arreter l'enregistrement (clic droit sur icone > C'est termine)
- Declencher un arret d'urgence (clic droit > ARRET D'URGENCE)
- Quitter Lea completement (clic droit > Quitter Lea)
- Desinstaller Lea via le panneau de configuration Windows
6. RESPONSABILITE
-----------------
L'utilisateur s'engage a ne pas utiliser Lea sur des donnees qu'il
n'est pas autorise a traiter dans le cadre de ses fonctions.
AIVANOV ne pourra etre tenu responsable d'un usage non conforme.
7. CONTACT
----------
Pour toute question ou demande d'acces/rectification/suppression
de donnees : dpo@aivanov.com
============================================================
En cliquant sur "J'accepte", vous confirmez avoir pris connaissance
de ces conditions et les accepter.
============================================================

554
deploy/installer/Lea.iss Normal file
View File

@@ -0,0 +1,554 @@
; ============================================================
; Lea.iss — Script Inno Setup pour l'installeur Lea
; ------------------------------------------------------------
; Compile avec Inno Setup 6.2+ (ISCC.exe Lea.iss)
;
; Ce script produit Lea-Setup-v{VERSION}.exe dans ..\releases\
;
; Fonctions principales :
; - Page de bienvenue + licence (CGU)
; - Page custom d'enrollment (nom, email, ID AIVANOV, URL, token)
; - Generation d'un machine_id unique par poste
; - Generation automatique de config.txt
; - Installation silencieuse de Python 3.12 embedded (optionnelle)
; - Raccourci demarrage automatique (checkbox)
; - Installation silencieuse : /VERYSILENT /CONFIG=path\to\config.txt
; - Desinstallation propre (kill process, cleanup, export logs)
;
; Pre-requis staging :
; Le dossier ..\build\installer_staging\ doit contenir :
; - Le package Lea complet (agent_v1/, lea_ui/, run_agent_v1.py, Lea.bat, ...)
; - Optionnel : python-3.12-embed\ (runtime Python embedded pre-configure)
; build_installer.sh s'occupe de preparer ce staging.
; ============================================================
#define MyAppName "Lea"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "AIVANOV"
#define MyAppURL "https://lea.labs.laurinebazin.design"
#define MyAppExeName "Lea.bat"
#define MyAppDescription "Lea - Assistante IA pour l'automatisation"
; Chemin du staging (peut etre surcharge via ISCC /DSourceDir=...)
#ifndef SourceDir
#define SourceDir "..\build\installer_staging"
#endif
; Chemin de sortie des installeurs
#ifndef OutputDir
#define OutputDir "..\releases"
#endif
; Activer le bundle Python embedded si present dans le staging
#define PythonEmbedDir "python-3.12-embed"
[Setup]
AppId={{B3F9A1E2-5C4D-4E7F-9A1B-2C3D4E5F6789}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputDir={#OutputDir}
OutputBaseFilename=Lea-Setup-v{#MyAppVersion}
; Compression correcte (pas trop aggressive pour que l'install reste rapide)
Compression=lzma2
SolidCompression=yes
; Support HiDPI
WizardStyle=modern
; Langue FR par defaut
ShowLanguageDialog=no
; Autorise l'install en mode user si pas admin (bascule sur LOCALAPPDATA)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
; Icone de l'installeur (decommenter si disponible)
; SetupIconFile=lea.ico
; Uninstall
UninstallDisplayName={#MyAppName} {#MyAppVersion}
; UninstallDisplayIcon={app}\lea.ico ; decommenter quand l'icone sera fournie
; Architecture : 64-bit uniquement (Windows 10+ / 11)
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; Version minimale Windows : 10
MinVersion=10.0
; Informations legales
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppDescription}
VersionInfoCopyright=Copyright (C) 2026 {#MyAppPublisher}
; Licence CGU affichee avant le choix du repertoire
LicenseFile=LICENSE.txt
[Languages]
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
[Files]
; Package complet (code Python + .bat + requirements)
; Note : install.bat EST copie (execute par [Run] pour creer le venv Python)
; Note : config.txt n'est PAS copie depuis le staging (il est genere par [Code])
Source: "{#SourceDir}\*"; \
DestDir: "{app}"; \
Flags: ignoreversion recursesubdirs createallsubdirs; \
Excludes: "{#PythonEmbedDir}\*,config.txt,*.log,sessions\*,__pycache__\*"
; Python 3.12 embedded (optionnel, copie conditionnelle via check)
Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \
DestDir: "{app}\python-embed"; \
Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist; \
Components: pythonembed
; Script de desinstallation custom (kill + export logs)
Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion
; Script de configuration du runtime Python embedded (optionnel)
Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion; Components: pythonembed
; Licence CGU (affichee dans la page licence ET conservee dans {app})
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
; Template de config pour installation silencieuse (reference)
Source: "config_template.txt"; DestDir: "{app}"; Flags: ignoreversion
[Components]
Name: "core"; Description: "Lea (obligatoire)"; Types: full compact custom; Flags: fixed
Name: "pythonembed"; Description: "Python 3.12 embedded (recommande si Python non installe sur le poste)"; Types: full
Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; Types: full
[Tasks]
Name: "desktopicon"; Description: "Creer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"; Flags: unchecked
Name: "startmenuicon"; Description: "Creer un raccourci dans le menu Demarrer"; GroupDescription: "Raccourcis :"
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startmenuicon
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon
; Raccourci autostart (shell:startup) — cree si composant autostart selectionne
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \
WorkingDir: "{app}"; Components: autostart
[Run]
; Apres copie : executer install.bat pour creer le venv et installer les dependances Python
; Skip si bundle embedded (dans ce cas, on utilise python-embed directement)
Filename: "{app}\install.bat"; \
WorkingDir: "{app}"; \
StatusMsg: "Installation des composants Python (1-2 minutes)..."; \
Flags: runhidden waituntilterminated; \
Components: not pythonembed
; Configuration Python embedded : creer un Lea.bat qui pointe sur python-embed
Filename: "{cmd}"; \
Parameters: "/c copy /y ""{app}\Lea.bat"" ""{app}\Lea.bat.bak"" && powershell -NoProfile -ExecutionPolicy Bypass -File ""{app}\configure_embed.ps1"""; \
WorkingDir: "{app}"; \
StatusMsg: "Configuration du runtime Python embedded..."; \
Flags: runhidden waituntilterminated skipifsilent; \
Components: pythonembed
; Lancer Lea a la fin de l'installation (optionnel)
Filename: "{app}\{#MyAppExeName}"; \
Description: "Lancer {#MyAppName} maintenant"; \
Flags: postinstall skipifsilent nowait shellexec
[UninstallRun]
; Tuer le process via PID du lock avant suppression des fichiers
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\uninstall_lea.ps1"" -AppDir ""{app}"""; \
RunOnceId: "KillLeaProcess"; \
Flags: runhidden waituntilterminated
[UninstallDelete]
Type: filesandordirs; Name: "{app}\.venv"
Type: filesandordirs; Name: "{app}\__pycache__"
Type: filesandordirs; Name: "{app}\agent_v1\__pycache__"
Type: filesandordirs; Name: "{app}\agent_v1\sessions"
Type: filesandordirs; Name: "{app}\agent_v1\logs"
Type: files; Name: "{app}\lea_agent.lock"
Type: files; Name: "{app}\config.txt"
Type: files; Name: "{app}\machine_id.txt"
; ============================================================
; Code Pascal : pages custom + generation config.txt + helpers
; ============================================================
[Code]
const
SERVER_URL_DEFAULT = 'https://lea.labs.laurinebazin.design/api/v1';
SERVER_HOST_DEFAULT = 'lea.labs.laurinebazin.design';
DEFAULT_TOKEN = '86031addb338e449fccdb1a983f61807aec15d42d482b9c7748ad607dc23caab';
var
EnrollmentPage: TInputQueryWizardPage;
TokenPage: TInputQueryWizardPage;
MachineIdValue: string;
ConfigFilePath: string;
// --------------------------------------------------------------------
// Helper : ajoute des guillemets autour d'une chaine
// --------------------------------------------------------------------
function AddQuotes(const S: string): string;
begin
Result := '"' + S + '"';
end;
// --------------------------------------------------------------------
// Wrapper CreateGUIDString (via PowerShell, fallback par defaut)
// --------------------------------------------------------------------
function CreateGUIDString(var Guid: string): Boolean;
var
ResultCode: Integer;
TmpFile: string;
Lines: TArrayOfString;
begin
Result := False;
TmpFile := ExpandConstant('{tmp}\guid.txt');
// powershell : genere un GUID
if Exec('powershell.exe',
'-NoProfile -Command "[guid]::NewGuid().ToString() | Out-File -Encoding ASCII ' + AddQuotes(TmpFile) + '"',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
if LoadStringsFromFile(TmpFile, Lines) and (GetArrayLength(Lines) > 0) then
begin
Guid := Trim(Lines[0]);
Result := Length(Guid) > 0;
end;
DeleteFile(TmpFile);
end;
end;
// --------------------------------------------------------------------
// Recupere le hostname de la machine
// --------------------------------------------------------------------
function GetComputerNameString(): string;
var
Buffer: string;
begin
Buffer := ExpandConstant('{computername}');
if Length(Buffer) = 0 then
Buffer := 'unknown-host';
Result := Buffer;
end;
// --------------------------------------------------------------------
// Genere un identifiant machine unique : UUID4 + hostname hashe
// --------------------------------------------------------------------
function GenerateMachineId(): string;
var
Guid: string;
Hostname: string;
I: Integer;
Hash: Cardinal;
begin
// Essaye d'utiliser le GUID genere par Windows (via PowerShell)
Guid := '';
if CreateGUIDString(Guid) then
Result := LowerCase(StringChange(StringChange(StringChange(Guid, '{', ''), '}', ''), '-', ''))
else
Result := IntToStr(GetTickCount);
// Ajoute un hash du hostname pour stabilite
Hostname := GetComputerNameString();
Hash := 0;
for I := 1 to Length(Hostname) do
Hash := (Hash * 31 + Ord(Hostname[I])) and $FFFFFFFF;
Result := Copy(Result, 1, 16) + '-' + Format('%08x', [Hash]);
end;
// --------------------------------------------------------------------
// Charge une configuration depuis /CONFIG=path (installation silencieuse)
// Format du fichier : NOM=valeur, une ligne par parametre
// Cles attendues : USER_NAME, USER_EMAIL, USER_ID, SERVER_URL, API_TOKEN
// --------------------------------------------------------------------
procedure LoadConfigFromCommandLine(); forward;
// --------------------------------------------------------------------
// Initialisation : cree les pages custom d'enrollment
// --------------------------------------------------------------------
procedure InitializeWizard();
begin
// Page 1 : informations collaborateur
EnrollmentPage := CreateInputQueryPage(wpSelectTasks,
'Identification du collaborateur',
'Veuillez renseigner vos informations pour l''enrollment',
'Ces informations sont envoyees au serveur Lea pour identifier votre poste. ' +
'Elles sont stockees de maniere securisee et ne sont jamais partagees avec des tiers.');
EnrollmentPage.Add('Nom et prenom :', False);
EnrollmentPage.Add('Email professionnel :', False);
EnrollmentPage.Add('ID interne AIVANOV (optionnel) :', False);
EnrollmentPage.Values[0] := '';
EnrollmentPage.Values[1] := '';
EnrollmentPage.Values[2] := '';
// Page 2 : configuration serveur (URL + token)
TokenPage := CreateInputQueryPage(EnrollmentPage.ID,
'Connexion au serveur Lea',
'Configuration de la connexion au serveur central',
'L''URL du serveur est pre-remplie par defaut. Le token d''authentification ' +
'vous est fourni par votre administrateur AIVANOV. Laissez la valeur par defaut ' +
'si vous ne savez pas quoi mettre.');
TokenPage.Add('URL du serveur (avec /api/v1) :', False);
TokenPage.Add('Token d''authentification :', False);
TokenPage.Values[0] := SERVER_URL_DEFAULT;
TokenPage.Values[1] := DEFAULT_TOKEN;
// Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir
LoadConfigFromCommandLine();
end;
// --------------------------------------------------------------------
// Implementation de LoadConfigFromCommandLine (declare en forward ci-dessus)
// --------------------------------------------------------------------
procedure LoadConfigFromCommandLine();
var
ConfigParam: string;
Lines: TArrayOfString;
I: Integer;
Line, Key, Value: string;
EqPos: Integer;
begin
ConfigParam := ExpandConstant('{param:CONFIG}');
if Length(ConfigParam) = 0 then Exit;
if not FileExists(ConfigParam) then Exit;
if not LoadStringsFromFile(ConfigParam, Lines) then Exit;
for I := 0 to GetArrayLength(Lines) - 1 do
begin
Line := Trim(Lines[I]);
if (Length(Line) = 0) or (Line[1] = '#') then Continue;
EqPos := Pos('=', Line);
if EqPos = 0 then Continue;
Key := Trim(Copy(Line, 1, EqPos - 1));
Value := Trim(Copy(Line, EqPos + 1, Length(Line)));
if Key = 'USER_NAME' then EnrollmentPage.Values[0] := Value
else if Key = 'USER_EMAIL' then EnrollmentPage.Values[1] := Value
else if Key = 'USER_ID' then EnrollmentPage.Values[2] := Value
else if Key = 'SERVER_URL' then TokenPage.Values[0] := Value
else if Key = 'API_TOKEN' then TokenPage.Values[1] := Value;
end;
end;
// --------------------------------------------------------------------
// Validation des pages custom (Nom/Email obligatoires, token non vide)
// --------------------------------------------------------------------
function NextButtonClick(CurPageID: Integer): Boolean;
var
Email: string;
begin
Result := True;
if CurPageID = EnrollmentPage.ID then
begin
if Length(Trim(EnrollmentPage.Values[0])) = 0 then
begin
MsgBox('Le nom est obligatoire.', mbError, MB_OK);
Result := False;
Exit;
end;
Email := Trim(EnrollmentPage.Values[1]);
if (Length(Email) = 0) or (Pos('@', Email) = 0) then
begin
MsgBox('Un email valide est obligatoire.', mbError, MB_OK);
Result := False;
Exit;
end;
end;
if CurPageID = TokenPage.ID then
begin
if Length(Trim(TokenPage.Values[0])) = 0 then
begin
MsgBox('L''URL du serveur est obligatoire.', mbError, MB_OK);
Result := False;
Exit;
end;
if Length(Trim(TokenPage.Values[1])) < 16 then
begin
if MsgBox('Le token parait court (< 16 caracteres). Continuer quand meme ?',
mbConfirmation, MB_YESNO) = IDNO then
begin
Result := False;
Exit;
end;
end;
end;
end;
// --------------------------------------------------------------------
// Ecrit config.txt genere dans le dossier d'installation
// --------------------------------------------------------------------
procedure WriteGeneratedConfig();
var
Config: string;
ServerUrl, ServerHost, Token: string;
UserName, UserEmail, UserId: string;
SlashPos: Integer;
begin
ConfigFilePath := ExpandConstant('{app}\config.txt');
ServerUrl := Trim(TokenPage.Values[0]);
Token := Trim(TokenPage.Values[1]);
UserName := Trim(EnrollmentPage.Values[0]);
UserEmail := Trim(EnrollmentPage.Values[1]);
UserId := Trim(EnrollmentPage.Values[2]);
// Derive ServerHost depuis ServerUrl : https://host/api/v1 -> host
ServerHost := ServerUrl;
ServerHost := StringChange(ServerHost, 'https://', '');
ServerHost := StringChange(ServerHost, 'http://', '');
SlashPos := Pos('/', ServerHost);
if SlashPos > 0 then
ServerHost := Copy(ServerHost, 1, SlashPos - 1);
Config :=
'# ============================================================' + #13#10 +
'# Configuration Lea (genere par l''installeur)' + #13#10 +
'# ============================================================' + #13#10 +
'# Genere le ' + GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':') + #13#10 +
'# Installe par : ' + UserName + ' <' + UserEmail + '>' + #13#10 +
'# ID interne : ' + UserId + #13#10 +
'# Machine ID : ' + MachineIdValue + #13#10 +
'# ============================================================' + #13#10 +
'' + #13#10 +
'# Adresse du serveur Lea (URL complete avec /api/v1)' + #13#10 +
'RPA_SERVER_URL=' + ServerUrl + #13#10 +
'' + #13#10 +
'# Cle d''authentification (fournie par l''administrateur)' + #13#10 +
'RPA_API_TOKEN=' + Token + #13#10 +
'' + #13#10 +
'# Nom du serveur (sans https://, sans /api/v1)' + #13#10 +
'RPA_SERVER_HOST=' + ServerHost + #13#10 +
'' + #13#10 +
'# Identifiant unique de cette machine (genere a l''install)' + #13#10 +
'RPA_MACHINE_ID=' + MachineIdValue + #13#10 +
'' + #13#10 +
'# Informations collaborateur (utilisees pour l''audit cote serveur)' + #13#10 +
'RPA_USER_NAME=' + UserName + #13#10 +
'RPA_USER_EMAIL=' + UserEmail + #13#10;
if Length(UserId) > 0 then
Config := Config + 'RPA_USER_ID=' + UserId + #13#10;
Config := Config + '' + #13#10 +
'# ============================================================' + #13#10 +
'# Parametres avances (ne pas modifier sauf indication)' + #13#10 +
'# ============================================================' + #13#10 +
'' + #13#10 +
'# Flouter les zones de texte dans les captures (securite donnees)' + #13#10 +
'RPA_BLUR_SENSITIVE=true' + #13#10 +
'' + #13#10 +
'# Duree de conservation des logs en jours (minimum 180 pour conformite)' + #13#10 +
'RPA_LOG_RETENTION_DAYS=180' + #13#10;
if not SaveStringToFile(ConfigFilePath, Config, False) then
MsgBox('Echec de l''ecriture de config.txt dans ' + ConfigFilePath, mbError, MB_OK);
end;
// --------------------------------------------------------------------
// Ecrit le machine_id.txt (identifiant du poste)
// --------------------------------------------------------------------
procedure WriteMachineId();
var
MachineIdFile: string;
begin
MachineIdFile := ExpandConstant('{app}\machine_id.txt');
if not SaveStringToFile(MachineIdFile, MachineIdValue, False) then
MsgBox('Echec de l''ecriture de machine_id.txt', mbError, MB_OK);
end;
// --------------------------------------------------------------------
// Notifie le serveur de l'enrollment (best-effort, non bloquant)
// POST vers {SERVER_URL}/agents/enroll avec les infos collaborateur
// --------------------------------------------------------------------
procedure NotifyServerEnrollment();
var
ResultCode: Integer;
PsScript: string;
PsFile: string;
ServerUrl, Token: string;
begin
ServerUrl := Trim(TokenPage.Values[0]);
Token := Trim(TokenPage.Values[1]);
PsFile := ExpandConstant('{tmp}\enroll.ps1');
PsScript :=
'$ErrorActionPreference = ''SilentlyContinue''' + #13#10 +
'$body = @{' + #13#10 +
' machine_id = ''' + MachineIdValue + '''' + #13#10 +
' hostname = $env:COMPUTERNAME' + #13#10 +
' user_name = ''' + EnrollmentPage.Values[0] + '''' + #13#10 +
' user_email = ''' + EnrollmentPage.Values[1] + '''' + #13#10 +
' user_id = ''' + EnrollmentPage.Values[2] + '''' + #13#10 +
' agent_version = ''' + '{#MyAppVersion}' + '''' + #13#10 +
'} | ConvertTo-Json' + #13#10 +
'try {' + #13#10 +
' Invoke-RestMethod -Uri ''' + ServerUrl + '/agents/enroll'' ' +
'-Method POST -Body $body -ContentType ''application/json'' ' +
'-Headers @{ Authorization = ''Bearer ' + Token + ''' } -TimeoutSec 10 | Out-Null' + #13#10 +
'} catch { exit 0 }' + #13#10;
SaveStringToFile(PsFile, PsScript, False);
Exec('powershell.exe',
'-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(PsFile),
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
DeleteFile(PsFile);
end;
// --------------------------------------------------------------------
// Hook : actions apres copie des fichiers (ssPostInstall)
// --------------------------------------------------------------------
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
begin
// Genere le machine_id AVANT la copie des fichiers
MachineIdValue := GenerateMachineId();
end;
if CurStep = ssPostInstall then
begin
// Ecrit config.txt et machine_id.txt
WriteGeneratedConfig();
WriteMachineId();
// Notifie le serveur (best-effort)
NotifyServerEnrollment();
end;
end;
// --------------------------------------------------------------------
// Desinstallation : proposer d'exporter les logs avant suppression
// --------------------------------------------------------------------
function InitializeUninstall(): Boolean;
var
LogDir, ExportDir: string;
ResultCode: Integer;
begin
Result := True;
LogDir := ExpandConstant('{app}\agent_v1\logs');
if DirExists(LogDir) then
begin
if MsgBox('Voulez-vous exporter les logs de Lea avant la desinstallation ?' + #13#10 +
'(les logs seront copies dans votre dossier Documents)',
mbConfirmation, MB_YESNO) = IDYES then
begin
ExportDir := ExpandConstant('{userdocs}\Lea_logs_export');
ForceDirectories(ExportDir);
Exec('powershell.exe',
'-NoProfile -Command "Copy-Item -Path ' + AddQuotes(LogDir + '\*') +
' -Destination ' + AddQuotes(ExportDir) + ' -Recurse -Force"',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
MsgBox('Logs exportes dans : ' + ExportDir, mbInformation, MB_OK);
end;
end;
end;

227
deploy/installer/README.md Normal file
View File

@@ -0,0 +1,227 @@
# Installeur Lea (Inno Setup)
Installeur Windows professionnel pour Lea, remplacant le ZIP + `install.bat` artisanal.
## Resume
Produit `Lea-Setup-v1.0.0.exe` dans `deploy/releases/`.
Caracteristiques :
- Interface francaise, moderne (style wizard Windows 10/11)
- Page custom d'enrollment (nom, email, ID interne, URL serveur, token)
- Generation automatique de `machine_id` unique (GUID + hash hostname)
- `config.txt` genere a partir des donnees saisies
- Option bundle Python 3.12 embedded (postes sans droits admin)
- Raccourci demarrage automatique (`shell:startup`) optionnel
- Notification serveur a l'install / desinstall (best-effort)
- Installation silencieuse : `/VERYSILENT /CONFIG=enroll.txt`
- Desinstallation propre : kill process, cleanup, export logs
## Pre-requis pour compiler
### Inno Setup 6.2+
Telecharger depuis [jrsoftware.org](https://jrsoftware.org/isinfo.php) et installer
`innosetup-6.x.exe`. Le compilateur `ISCC.exe` doit etre accessible.
### Alternative Linux : Wine
```bash
# Installation
winetricks innosetup
# Ou : telecharger innosetup-6.x.exe et lancer : wine innosetup-6.x.exe
# Verifier
ls "$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe"
```
Le script `build_installer.sh` detecte automatiquement Wine si present.
## Build local
### Build complet (staging + compilation)
```bash
cd rpa_vision_v3
./deploy/installer/build_installer.sh
```
Produit `deploy/releases/Lea-Setup-v1.0.0.exe`.
### Build staging uniquement (sans ISCC)
```bash
./deploy/installer/build_installer.sh --stage-only
```
Prepare `deploy/build/installer_staging/` puis affiche la commande ISCC a executer
sur Windows.
### Nettoyer avant
```bash
./deploy/installer/build_installer.sh --clean
```
## Build sur Windows (recommande pour production)
1. Copier le dossier `deploy/` sur le PC Windows
2. Ouvrir `deploy/installer/Lea.iss` dans Inno Setup Compiler
3. `Build > Compile` (ou F9)
4. Recuperer `deploy/releases/Lea-Setup-v1.0.0.exe`
## Python 3.12 embedded (optionnel)
Pour bundler Python directement dans l'installeur (evite d'exiger que les postes
aient Python installe) :
```bash
# Sur Linux
cd deploy/installer
wget https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip
mkdir python-3.12-embed
unzip python-3.12.8-embed-amd64.zip -d python-3.12-embed/
```
Le staging copie automatiquement ce dossier si present. Le composant
"pythonembed" devient alors selectionnable dans l'installeur.
Le script `configure_embed.ps1` :
1. Patche `python312._pth` pour activer `import site`
2. Installe `pip` via `get-pip.py`
3. Installe `requirements_agent.txt`
4. Reecrit `Lea.bat` pour pointer sur `python-embed\pythonw.exe`
## Installation silencieuse (deploiement de masse)
Pour deployer sans interaction utilisateur (GPO, SCCM, script PowerShell) :
1. Preparer un fichier `enroll.txt` par poste (ou un commun) :
```
USER_NAME=Jean Dupont
USER_EMAIL=jean.dupont@aivanov.com
USER_ID=EMP-00123
SERVER_URL=https://lea.labs.laurinebazin.design/api/v1
API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
2. Lancer l'installeur :
```cmd
Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=C:\temp\enroll.txt /DIR="C:\Lea"
```
Parametres Inno Setup utiles :
- `/VERYSILENT` : aucune UI
- `/SILENT` : barre de progression seulement
- `/DIR="..."` : dossier d'installation
- `/LOG="install.log"` : log d'installation
- `/TASKS="startmenuicon,autostart"` : composants a installer (voir `[Tasks]` et `[Components]`)
- `/CONFIG=path` : fichier d'enrollment (custom, specifique a Lea)
## Signature du .exe (SmartScreen)
Sans signature, Windows SmartScreen affiche un avertissement rouge ("Cet editeur
est inconnu"). Pour eviter cela, signer l'installeur avec un certificat
code-signing.
### Options de certificat
1. **Certificat OV (Organization Validation)** : ~200-400 EUR/an
- Sectigo, DigiCert, GlobalSign
- SmartScreen apprend la reputation progressivement (~30 installations)
- Livre sur token USB FIPS depuis 2023
2. **Certificat EV (Extended Validation)** : ~400-700 EUR/an
- Reputation SmartScreen immediate (pas d'avertissement des la 1ere install)
- Strict : obligatoirement sur token USB
### Signature manuelle (avec signtool.exe du Windows SDK)
```cmd
signtool sign ^
/tr http://timestamp.sectigo.com ^
/td sha256 ^
/fd sha256 ^
/a ^
"deploy\releases\Lea-Setup-v1.0.0.exe"
signtool verify /pa /v "deploy\releases\Lea-Setup-v1.0.0.exe"
```
### Signature automatique dans Inno Setup
Ajouter dans `Lea.iss` apres `[Setup]` :
```
SignTool=signtool $f
```
Et declarer le signtool via `ISCC.exe /Ssigntool=...` au build.
### Solution interne (certif AIVANOV)
Si AIVANOV a deja un certificat code-signing, le token USB + mot de passe
suffisent. Sinon, Sectigo OV est un bon choix d'entree de gamme.
## Structure du dossier installer/
```
deploy/installer/
├── Lea.iss # Script Inno Setup principal
├── build_installer.sh # Helper bash (staging + ISCC)
├── uninstall_lea.ps1 # Script de desinstallation propre
├── configure_embed.ps1 # Configuration Python embedded
├── config_template.txt # Modele config pour /VERYSILENT /CONFIG=
├── LICENSE.txt # CGU affichees dans la page licence
└── README.md # Ce fichier
```
## Test de l'installeur
1. **Machine de test Windows 11** (VM ou PC physique, idealement sans Python)
2. Copier `Lea-Setup-v1.0.0.exe` sur la machine
3. Double-cliquer : verifier que l'enrollment s'affiche en francais
4. Tester l'installation (avec et sans Python deja installe)
5. Verifier le fichier `C:\Program Files\Lea\config.txt` genere
6. Verifier le raccourci `shell:startup` (si option cochee)
7. Lancer Lea, verifier la connexion au serveur
8. Tester la desinstallation depuis "Ajout/suppression de programmes"
### Test automatise (PowerShell, sur la VM)
```powershell
# Installation silencieuse
$cfg = "C:\temp\enroll.txt"
@"
USER_NAME=Test Automatique
USER_EMAIL=test@aivanov.com
"@ | Out-File -Encoding ASCII $cfg
.\Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=$cfg /LOG="C:\temp\install.log"
# Verifications
Test-Path "C:\Program Files\Lea\config.txt"
Get-Content "C:\Program Files\Lea\machine_id.txt"
# Desinstallation silencieuse
$uninst = Get-WmiObject Win32_Product | Where-Object { $_.Name -like "Lea*" }
$uninst.Uninstall()
```
## Notes et limites connues
- **Endpoint serveur `/agents/enroll` et `/agents/uninstall` :** pas encore
implemente cote serveur (avril 2026). L'installeur envoie la requete en
best-effort, une erreur est silencieusement ignoree. A implementer dans
`agent_v0/server_v1/api_stream.py` quand necessaire.
- **Python embedded :** le patch `python312._pth` + pip bootstrap fonctionne mais
augmente la taille de l'installeur (~25 MB). A reserver aux postes sans
Python.
- **Code signing :** indispensable pour deploiement hopital/client. Prevoir le
budget certificat (400-700 EUR/an) dans la roadmap commerciale.
## Historique
- v1.0.0 (2026-04-13) : Premiere version de l'installeur Inno Setup.

View File

@@ -0,0 +1,220 @@
#!/bin/bash
# ============================================================
# build_installer.sh — Prepare le staging et invoque ISCC
# ------------------------------------------------------------
#
# Ce script :
# 1. Invoque build_package.sh pour generer le package classique
# 2. Copie le package dans deploy/build/installer_staging/
# 3. Copie les helpers de l'installeur (uninstall, licence)
# 4. Appelle Inno Setup (ISCC.exe) si disponible
# (sinon, affiche les instructions pour compiler sous Windows)
#
# Usage :
# ./deploy/installer/build_installer.sh # Build complet
# ./deploy/installer/build_installer.sh --stage-only # Prepare le staging uniquement
# ./deploy/installer/build_installer.sh --clean # Nettoyer avant
#
# Pre-requis :
# - bash, rsync, zip (pour le package de base)
# - Inno Setup 6.2+ installe (Windows ou Wine) pour compiler
#
# Sur Linux, ISCC.exe peut etre execute via Wine :
# wine "/home/dom/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" Lea.iss
# ============================================================
set -euo pipefail
# Couleurs
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
PROJECT_ROOT="$(dirname "$DEPLOY_DIR")"
STAGING_DIR="$DEPLOY_DIR/build/installer_staging"
RELEASES_DIR="$DEPLOY_DIR/releases"
BASE_BUILD_DIR="$DEPLOY_DIR/build/Lea"
# Recupere la version depuis config.py
VERSION=$(grep -oP 'AGENT_VERSION\s*=\s*"([^"]+)"' "$PROJECT_ROOT/agent_v0/agent_v1/config.py" | grep -oP '"[^"]+"' | tr -d '"' || echo "1.0.0")
echo -e "${GREEN}============================================================${NC}"
echo -e "${GREEN} Build installeur Inno Setup Lea v${VERSION}${NC}"
echo -e "${GREEN}============================================================${NC}"
echo ""
# ---------------------------------------------------------------
# Parsing des arguments
# ---------------------------------------------------------------
STAGE_ONLY=0
CLEAN=0
for arg in "$@"; do
case "$arg" in
--stage-only) STAGE_ONLY=1 ;;
--clean) CLEAN=1 ;;
*) echo "Argument inconnu : $arg" ;;
esac
done
# ---------------------------------------------------------------
# 1. Clean optionnel
# ---------------------------------------------------------------
if [[ $CLEAN -eq 1 ]]; then
echo -e "${YELLOW}[0/5] Nettoyage des anciens builds...${NC}"
rm -rf "$STAGING_DIR"
rm -rf "$BASE_BUILD_DIR"
rm -f "$RELEASES_DIR"/Lea-Setup-*.exe
echo " OK"
echo ""
fi
mkdir -p "$RELEASES_DIR"
# ---------------------------------------------------------------
# 2. Build du package de base (reutilise build_package.sh)
# ---------------------------------------------------------------
echo "[1/5] Build du package de base..."
if [[ ! -d "$BASE_BUILD_DIR" ]] || [[ $CLEAN -eq 1 ]]; then
bash "$DEPLOY_DIR/build_package.sh" >/dev/null
fi
if [[ ! -d "$BASE_BUILD_DIR" ]]; then
echo -e "${RED} Erreur : $BASE_BUILD_DIR n'a pas ete cree par build_package.sh${NC}"
exit 1
fi
echo " Package de base pret : $BASE_BUILD_DIR"
echo ""
# ---------------------------------------------------------------
# 3. Copie vers staging
# ---------------------------------------------------------------
echo "[2/5] Preparation du staging installeur..."
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
# Copie tout sauf config.txt (genere par l'installeur) et install.bat
# install.bat est conserve mais sera appele en mode silencieux par ISS
rsync -a \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='.venv' \
--exclude='sessions/' \
--exclude='logs/' \
"$BASE_BUILD_DIR/" \
"$STAGING_DIR/"
# On supprime le config.txt du staging : c'est l'installeur qui le generera
rm -f "$STAGING_DIR/config.txt"
echo " Staging : $STAGING_DIR"
echo " Fichiers : $(find "$STAGING_DIR" -type f | wc -l)"
echo ""
# ---------------------------------------------------------------
# 4. Copie des helpers installeur (uninstall, licence, etc.)
# ---------------------------------------------------------------
echo "[3/5] Copie des helpers installeur..."
cp "$SCRIPT_DIR/uninstall_lea.ps1" "$STAGING_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/configure_embed.ps1" "$STAGING_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/LICENSE.txt" "$STAGING_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/config_template.txt" "$STAGING_DIR/config_template.txt" 2>/dev/null || true
echo " Helpers copies"
echo ""
# ---------------------------------------------------------------
# 5. Python embedded (optionnel)
# ---------------------------------------------------------------
PYTHON_EMBED_SRC="${PYTHON_EMBED_DIR:-$SCRIPT_DIR/python-3.12-embed}"
if [[ -d "$PYTHON_EMBED_SRC" ]]; then
echo "[4/5] Copie de Python 3.12 embedded..."
rsync -a "$PYTHON_EMBED_SRC/" "$STAGING_DIR/python-3.12-embed/"
echo " Python embedded inclus"
else
echo -e "${YELLOW}[4/5] Python 3.12 embedded non trouve dans $PYTHON_EMBED_SRC${NC}"
echo " L'installeur sera produit SANS bundle Python."
echo " Pour bundler Python : voir README.md section 'Python embedded'"
fi
echo ""
# ---------------------------------------------------------------
# 6. Stage-only : on s'arrete ici
# ---------------------------------------------------------------
if [[ $STAGE_ONLY -eq 1 ]]; then
echo -e "${GREEN} Staging pret. Utiliser ISCC pour compiler :${NC}"
echo " ISCC.exe \"$SCRIPT_DIR/Lea.iss\""
echo ""
exit 0
fi
# ---------------------------------------------------------------
# 7. Compilation avec ISCC (si disponible)
# ---------------------------------------------------------------
echo "[5/5] Compilation Inno Setup..."
# Chercher ISCC : natif Linux (rare), Wine, ou WSL
ISCC_BIN=""
if command -v iscc >/dev/null 2>&1; then
ISCC_BIN="iscc"
elif command -v ISCC.exe >/dev/null 2>&1; then
ISCC_BIN="ISCC.exe"
elif command -v wine >/dev/null 2>&1; then
# Chemins Wine courants
for path in \
"$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" \
"$HOME/.wine/drive_c/Program Files/Inno Setup 6/ISCC.exe"; do
if [[ -f "$path" ]]; then
ISCC_BIN="wine \"$path\""
break
fi
done
fi
if [[ -z "$ISCC_BIN" ]]; then
echo ""
echo -e "${YELLOW} ISCC (Inno Setup Compiler) introuvable.${NC}"
echo ""
echo " Le staging est pret dans : $STAGING_DIR"
echo ""
echo " Pour compiler l'installeur, deux options :"
echo ""
echo " 1) Sur un PC Windows avec Inno Setup 6 installe :"
echo " - Copier le dossier deploy/ sur le PC"
echo " - Ouvrir deploy/installer/Lea.iss dans Inno Setup"
echo " - Cliquer 'Compile' (F9)"
echo " - Recuperer deploy/releases/Lea-Setup-v${VERSION}.exe"
echo ""
echo " 2) Sur Linux avec Wine :"
echo " - winetricks innosetup (ou installer le .exe manuellement)"
echo " - wine \"\$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe\" \\"
echo " \"$SCRIPT_DIR/Lea.iss\""
echo ""
exit 0
fi
echo " ISCC trouve : $ISCC_BIN"
eval "$ISCC_BIN \"$SCRIPT_DIR/Lea.iss\""
# Verification du resultat
OUTPUT_EXE="$RELEASES_DIR/Lea-Setup-v${VERSION}.exe"
if [[ -f "$OUTPUT_EXE" ]]; then
EXE_SIZE=$(du -h "$OUTPUT_EXE" | cut -f1)
echo ""
echo -e "${GREEN}============================================================${NC}"
echo -e "${GREEN} Installeur produit !${NC}"
echo -e "${GREEN}============================================================${NC}"
echo ""
echo " Fichier : $OUTPUT_EXE"
echo " Taille : $EXE_SIZE"
echo ""
echo " Deploiement :"
echo " - Signer le .exe avec un certificat code-signing (voir README.md)"
echo " - Publier sur : https://lea.labs.laurinebazin.design/downloads/"
echo " - Installation silencieuse : Lea-Setup-v${VERSION}.exe /VERYSILENT /CONFIG=enroll.txt"
echo ""
else
echo -e "${RED} Erreur : $OUTPUT_EXE n'a pas ete produit${NC}"
exit 1
fi

View File

@@ -0,0 +1,27 @@
# ============================================================
# config_template.txt — Modele pour installation silencieuse
# ------------------------------------------------------------
#
# Ce fichier est utilise en mode /VERYSILENT pour pre-remplir
# les valeurs d'enrollment sans interface graphique.
#
# Usage :
# Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=enroll.txt
#
# L'installeur lit ce fichier au demarrage et remplit les pages
# custom (nom, email, ID, URL, token) automatiquement.
#
# Toutes les cles ci-dessous sont optionnelles. Si une cle est
# absente, la valeur par defaut de l'installeur est utilisee.
#
# Format : CLE=valeur, une ligne par parametre, # = commentaire.
# ============================================================
# Identite du collaborateur (obligatoires sauf USER_ID)
USER_NAME=Prenom Nom
USER_EMAIL=prenom.nom@aivanov.com
USER_ID=
# Connexion serveur (valeurs par defaut deja pre-remplies)
SERVER_URL=https://lea.labs.laurinebazin.design/api/v1
API_TOKEN=86031addb338e449fccdb1a983f61807aec15d42d482b9c7748ad607dc23caab

View File

@@ -0,0 +1,112 @@
# ============================================================
# configure_embed.ps1 — Configure le runtime Python embedded
# ------------------------------------------------------------
#
# Quand le composant 'pythonembed' est installe, on a :
# <AppDir>\python-embed\ <-- runtime Python 3.12 embedded
#
# Ce script :
# 1. Active l'import des packages (patch de python312._pth)
# 2. Installe pip dans le runtime embedded
# 3. Installe les dependances requirements_agent.txt
# 4. Reecrit Lea.bat pour pointer sur python-embed\pythonw.exe
#
# Doit etre execute avec le CWD = <AppDir>
# ============================================================
$ErrorActionPreference = 'Stop'
$AppDir = Get-Location
$EmbedDir = Join-Path $AppDir "python-embed"
$PythonExe = Join-Path $EmbedDir "python.exe"
if (-not (Test-Path $PythonExe)) {
Write-Host "Python embedded introuvable, abandon."
exit 1
}
Write-Host "Configuration de Python embedded..."
# ---------------------------------------------------------------
# 1. Decommenter la ligne 'import site' dans python312._pth
# (necessaire pour que pip puisse fonctionner)
# ---------------------------------------------------------------
$PthFile = Get-ChildItem -Path $EmbedDir -Filter "python*._pth" | Select-Object -First 1
if ($PthFile) {
$Content = Get-Content $PthFile.FullName
$NewContent = $Content -replace '^#import site', 'import site'
Set-Content -Path $PthFile.FullName -Value $NewContent
Write-Host " python._pth patche (import site active)"
}
# ---------------------------------------------------------------
# 2. Installer pip (bootstrap via get-pip.py)
# ---------------------------------------------------------------
$GetPip = Join-Path $env:TEMP "get-pip.py"
Write-Host " Telechargement de get-pip.py..."
Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPip -UseBasicParsing
Write-Host " Installation de pip..."
& $PythonExe $GetPip --no-warn-script-location
Remove-Item $GetPip -Force
# ---------------------------------------------------------------
# 3. Installer les dependances
# ---------------------------------------------------------------
$Requirements = Join-Path $AppDir "requirements_agent.txt"
if (Test-Path $Requirements) {
Write-Host " Installation des dependances Python..."
& $PythonExe -m pip install --no-warn-script-location -r $Requirements
}
# ---------------------------------------------------------------
# 4. Reecrire Lea.bat pour utiliser python-embed
# ---------------------------------------------------------------
$LeaBat = Join-Path $AppDir "Lea.bat"
$NewLeaBat = @"
@echo off
chcp 65001 >nul 2>&1
title Lea - Assistante IA
cd /d "%~dp0"
if exist "lea_agent.lock" (
for /f "usebackq tokens=* delims=" %%i in ("lea_agent.lock") do (
taskkill /F /PID %%i >nul 2>&1
)
del /f /q "lea_agent.lock" >nul 2>&1
timeout /t 2 >nul
)
if exist "config.txt" (
for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do (
if not "%%a"=="" if not "%%b"=="" set "%%a=%%b"
)
)
echo.
echo Demarrage de Lea (runtime embedded)...
echo.
start "" /b "%~dp0python-embed\pythonw.exe" run_agent_v1.py
timeout /t 3 >nul
set "LEA_ALIVE=0"
if exist "lea_agent.lock" (
for /f "usebackq tokens=* delims=" %%i in ("lea_agent.lock") do (
tasklist /FI "PID eq %%i" /NH 2>nul | findstr /I "pythonw" >nul && set "LEA_ALIVE=1"
)
)
if "%LEA_ALIVE%"=="0" (
echo.
echo Lea n'a pas demarre correctement. Affichage des erreurs :
echo.
"%~dp0python-embed\python.exe" run_agent_v1.py
pause
)
"@
Set-Content -Path $LeaBat -Value $NewLeaBat -Encoding ASCII
Write-Host " Lea.bat reecrit pour runtime embedded"
Write-Host "Configuration terminee."
exit 0

View File

@@ -0,0 +1,99 @@
# ============================================================
# uninstall_lea.ps1 — Script de desinstallation propre de Lea
# ------------------------------------------------------------
#
# Appele par Inno Setup via [UninstallRun] AVANT la suppression
# des fichiers. Roles :
#
# 1. Tuer proprement le process Lea (via PID du lock)
# 2. Nettoyer shell:startup (supprimer le raccourci auto-start)
# 3. Notifier le serveur de la desinstallation (best-effort)
# 4. Supprimer le lock file
#
# Usage (par Inno Setup) :
# powershell.exe -NoProfile -ExecutionPolicy Bypass \
# -File uninstall_lea.ps1 -AppDir "C:\Program Files\Lea"
# ============================================================
param(
[Parameter(Mandatory = $true)]
[string]$AppDir
)
$ErrorActionPreference = 'SilentlyContinue'
Write-Host "Desinstallation de Lea en cours..."
# ---------------------------------------------------------------
# 1. Tuer le process via PID du lock file
# ---------------------------------------------------------------
$LockFile = Join-Path $AppDir "lea_agent.lock"
if (Test-Path $LockFile) {
try {
$Pid = (Get-Content $LockFile -ErrorAction Stop | Select-Object -First 1).Trim()
if ($Pid -match '^\d+$') {
Write-Host " Arret du process Lea (PID $Pid)..."
Stop-Process -Id ([int]$Pid) -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
} catch {
Write-Host " Lock file illisible (ignore)."
}
Remove-Item $LockFile -Force -ErrorAction SilentlyContinue
}
# ---------------------------------------------------------------
# 2. Nettoyer shell:startup (peut ne pas exister si composant
# non installe, on le supprime silencieusement dans tous les cas)
# ---------------------------------------------------------------
$StartupDir = [Environment]::GetFolderPath('Startup')
$StartupShortcut = Join-Path $StartupDir "Lea.lnk"
if (Test-Path $StartupShortcut) {
Write-Host " Suppression du raccourci auto-start..."
Remove-Item $StartupShortcut -Force -ErrorAction SilentlyContinue
}
# ---------------------------------------------------------------
# 3. Notifier le serveur de la desinstallation (best-effort)
# ---------------------------------------------------------------
$ConfigFile = Join-Path $AppDir "config.txt"
$MachineIdFile = Join-Path $AppDir "machine_id.txt"
if ((Test-Path $ConfigFile) -and (Test-Path $MachineIdFile)) {
try {
$ConfigLines = Get-Content $ConfigFile
$ServerUrl = ($ConfigLines | Where-Object { $_ -match '^RPA_SERVER_URL=' } | Select-Object -First 1) -replace '^RPA_SERVER_URL=', ''
$Token = ($ConfigLines | Where-Object { $_ -match '^RPA_API_TOKEN=' } | Select-Object -First 1) -replace '^RPA_API_TOKEN=', ''
$MachineId = (Get-Content $MachineIdFile -ErrorAction Stop | Select-Object -First 1).Trim()
if ($ServerUrl -and $Token -and $MachineId) {
Write-Host " Notification du serveur..."
$Body = @{
machine_id = $MachineId
hostname = $env:COMPUTERNAME
event = 'uninstall'
timestamp = (Get-Date -Format "o")
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "$ServerUrl/agents/uninstall" `
-Method POST `
-Body $Body `
-ContentType 'application/json' `
-Headers @{ Authorization = "Bearer $Token" } `
-TimeoutSec 5 `
-ErrorAction SilentlyContinue | Out-Null
}
} catch {
# Best-effort : on ignore toute erreur reseau/auth
Write-Host " Notification serveur echouee (ignore)."
}
}
# ---------------------------------------------------------------
# 4. Supprimer les fichiers restants verrouilles eventuellement
# ---------------------------------------------------------------
Start-Sleep -Seconds 1
Get-ChildItem -Path $AppDir -Filter "*.pyc" -Recurse -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
Write-Host "Desinstallation : pre-traitement termine."
exit 0