Files
rpa_vision_v3/deploy/installer/Lea.iss
Dom ab78ae390a chore(version): bump 1.0.1 -> 1.0.2 (fixes client + installeur upgrade-safe)
Nouvelle politique : versionner chaque livrable. 1.0.2 = httpx embed +
capture JPEG + watchdog RDP + MAJ silencieuse (OFF) + installeur voie 1
(preserve identite, tue Lea, backup, purge). Source de verite = config.py
(AGENT_VERSION) + Lea.iss (MyAppVersion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:06:39 +02:00

681 lines
26 KiB
Plaintext

; ============================================================
; 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.2"
#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 EXCLU du staging (runtime 100% embedded, plus de venv/pip)
; 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 (OBLIGATOIRE — runtime 100% autonome, aucune dependance Python systeme)
Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \
DestDir: "{app}\python-embed"; \
Flags: ignoreversion recursesubdirs createallsubdirs
; Script de desinstallation custom (kill + export logs)
Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion
; Script de configuration du runtime Python embedded (toujours installe)
Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion
; 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]
; Composant unique fixe : pas de choix utilisateur (runtime embedded toujours inclus).
; Inno masque la page Composants quand il n'y a aucun composant selectionnable.
Name: "core"; Description: "Lea"; Types: full compact custom; Flags: fixed
[Tasks]
Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; GroupDescription: "Options :"
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 tache autostart selectionnee
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \
WorkingDir: "{app}"; Tasks: autostart
[Run]
; Configuration du runtime embedded : reecrit Lea.bat pour pointer sur python-embed.
; TOUJOURS execute — runtime 100% autonome, aucune branche venv/pip/Python systeme.
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 de Lea..."; \
Flags: runhidden waituntilterminated
; 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}\python-embed"
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}\config.txt.bak.*"
Type: files; Name: "{app}\machine_id.txt"
Type: files; Name: "{app}\Lea.bat.bak"
Type: files; Name: "{app}\install.bat"
; Filet de securite : supprime tout residu genere au runtime (caches, *.pyc, logs)
; afin que le dossier applicatif soit entierement supprime (exigence desinstall propre).
Type: filesandordirs; Name: "{app}"
; ============================================================
; 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 = 'o3_LHqV_7_Gc6OVPHndhsBbvG6HJ5PCgl8yIBhGUIz8';
var
EnrollmentPage: TInputQueryWizardPage;
TokenPage: TInputQueryWizardPage;
MachineIdValue: string;
ConfigFilePath: string;
ExistingMachineId: 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
begin
StringChange(Guid, '{', '');
StringChange(Guid, '}', '');
StringChange(Guid, '-', '');
Result := LowerCase(Guid);
end
else
Result := GetDateTimeString('yyyymmddhhnnss', #0, #0);
// 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;
// --------------------------------------------------------------------
// UPGRADE — trouve le dossier d'une install Lea existante (config.txt present)
// --------------------------------------------------------------------
function FindExistingInstallDir(): string;
var
Candidates: array[0..1] of string;
I: Integer;
begin
Result := '';
Candidates[0] := ExpandConstant('{localappdata}\Programs\Lea');
Candidates[1] := ExpandConstant('{autopf}\Lea');
for I := 0 to 1 do
begin
if FileExists(Candidates[I] + '\config.txt') then
begin
Result := Candidates[I];
Exit;
end;
end;
end;
// --------------------------------------------------------------------
// UPGRADE — lit le config.txt existant : pre-remplit le wizard avec la
// VRAIE conf du poste (serveur/token/user) et MEMORISE le machine_id pour
// le PRESERVER (ne pas regenerer une nouvelle identite fleet).
// --------------------------------------------------------------------
procedure LoadExistingConfig();
var
Dir, ConfPath: string;
Lines: TArrayOfString;
I, EqPos: Integer;
Line, Key, Value: string;
begin
ExistingMachineId := '';
Dir := FindExistingInstallDir();
if Dir = '' then Exit; // install neuve -> comportement par defaut
ConfPath := Dir + '\config.txt';
if LoadStringsFromFile(ConfPath, Lines) then
begin
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 = 'RPA_SERVER_URL' then TokenPage.Values[0] := Value
else if Key = 'RPA_API_TOKEN' then TokenPage.Values[1] := Value
else if Key = 'RPA_USER_NAME' then EnrollmentPage.Values[0] := Value
else if Key = 'RPA_USER_EMAIL' then EnrollmentPage.Values[1] := Value
else if Key = 'RPA_USER_ID' then EnrollmentPage.Values[2] := Value
else if Key = 'RPA_MACHINE_ID' then ExistingMachineId := Value;
end;
end;
// Fallback : machine_id.txt si absent du config.txt
if (ExistingMachineId = '') and FileExists(Dir + '\machine_id.txt') then
begin
if LoadStringsFromFile(Dir + '\machine_id.txt', Lines) and (GetArrayLength(Lines) > 0) then
ExistingMachineId := Trim(Lines[0]);
end;
end;
// --------------------------------------------------------------------
// 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;
// UPGRADE : si une install existe, pre-remplir avec SA config (pas les
// defauts) et memoriser son machine_id pour le preserver.
LoadExistingConfig();
// Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir (prioritaire)
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;
StringChange(ServerHost, 'https://', '');
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;
// --------------------------------------------------------------------
// UPGRADE — AVANT la copie des fichiers : tuer une Lea en cours (via le
// PID du lock) pour liberer les DLL de python-embed. Evite une install
// partielle / "reboot required". Ne tue QUE le PID du lock (jamais tous
// les pythonw du poste).
// --------------------------------------------------------------------
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
AppDir, LockPath, BackupDir, SessionsDir: string;
Lines: TArrayOfString;
ResultCode: Integer;
begin
Result := '';
AppDir := ExpandConstant('{app}');
// 1) Tuer une Lea en cours (via le PID du lock) pour liberer les DLL
// python-embed. Ne tue QUE ce PID, jamais tous les pythonw du poste.
LockPath := AppDir + '\lea_agent.lock';
if FileExists(LockPath) then
begin
if LoadStringsFromFile(LockPath, Lines) and (GetArrayLength(Lines) > 0) then
Exec('taskkill.exe', '/F /PID ' + Trim(Lines[0]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
DeleteFile(LockPath);
Sleep(1500);
end;
// UPGRADE uniquement (install existante detectee via config.txt).
if FileExists(AppDir + '\config.txt') then
begin
// 2) BACKUP (rollback) : copie code+config vers <app>_backup, HORS
// python-embed / sessions / logs (leger, rapide). Filet si la nouvelle
// version deconne : Julien restaure ce dossier.
BackupDir := AppDir + '_backup';
Exec(ExpandConstant('{cmd}'),
'/c rmdir /s /q "' + BackupDir + '" 2>nul & robocopy "' + AppDir + '" "' + BackupDir +
'" /E /XD python-embed sessions logs __pycache__ /XF *.pyc /R:1 /W:1 /NFL /NDL /NJH /NJS /NP >nul 2>&1',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// 3) PURGE des captures accumulees (donnees d'apprentissage internes, non
// exploitables cote clinique) : libere le disque. Le fix capture JPEG
// evite que la saturation reprenne. Les logs (compliance 180j) restent.
SessionsDir := AppDir + '\agent_v1\sessions';
if DirExists(SessionsDir) then
Exec(ExpandConstant('{cmd}'),
'/c rmdir /s /q "' + SessionsDir + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
// --------------------------------------------------------------------
// Hook : actions apres copie des fichiers (ssPostInstall)
// --------------------------------------------------------------------
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
begin
// UPGRADE : preserver l'identite existante ; sinon en generer une neuve.
if ExistingMachineId <> '' then
MachineIdValue := ExistingMachineId
else
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;