@@ -59,11 +59,10 @@ Name: "full"; Description: "Full installation"; Flags: iscustom;
5959Type : files ; Name : " {app} \ucrtbase.dll"
6060
6161[Files]
62- ; All files go to {app} — no service, no staging install flow
63- DestDir : " {app} " ; Flags : ignoreversion recursesubdirs ; Source :" {#LayoutDir}\*"
62+ ; Versioned install: all files go to {app}\Versions\{version}, no service install
63+ DestDir : " {app} \Versions\{#MyAppInstallerVersion} " ; Flags : ignoreversion recursesubdirs ; Source :" {#LayoutDir}\*"
6464
6565[Dirs]
66- ; No longer creating service ProgramData directory — not using service
6766
6867[UninstallDelete]
6968; Deletes the entire installation directory, including files and subdirectories
@@ -72,8 +71,8 @@ Type: filesandordirs; Name: "{commonappdata}\GVFS\GVFS.Upgrade";
7271
7372[Registry]
7473Root : HKLM; Subkey : " {#EnvironmentKey}" ; \
75- ValueType : expandsz ; ValueName : " PATH" ; ValueData : " {olddata};{app} " ; \
76- Check : NeedsAddPath(ExpandConstant(' {app} ' ))
74+ ValueType : expandsz ; ValueName : " PATH" ; ValueData : " {olddata};{app} \Current " ; \
75+ Check : NeedsAddPath(ExpandConstant(' {app} \Current ' ))
7776
7877Root : HKLM; Subkey : " {#FileSystemKey}" ; \
7978 ValueType : dword ; ValueName : " NtfsEnableDetailedCleanupResults" ; ValueData : " 1" ; \
@@ -254,7 +253,7 @@ procedure WriteOnDiskVersion16CapableFile();
254253var
255254 FilePath: string;
256255begin
257- FilePath := ExpandConstant(' {app}\OnDiskVersion16CapableInstallation.dat' );
256+ FilePath := ExpandConstant(' {app}\Versions\{#MyAppInstallerVersion}\ OnDiskVersion16CapableInstallation.dat' );
258257 if not FileExists(FilePath) then
259258 begin
260259 Log(' WriteOnDiskVersion16CapableFile: Writing file ' + FilePath);
@@ -278,14 +277,6 @@ begin
278277
279278 try
280279 Log(' RegisterAutoMountLogonTask: Getting user SID' );
281- if not Exec(' powershell.exe' , ' -NoProfile "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value"' , ' ' , SW_HIDE, ewWaitUntilTerminated, ResultCode) then
282- begin
283- Log(' RegisterAutoMountLogonTask: Failed to get user SID' );
284- exit;
285- end ;
286-
287- // Read user SID from temp file (powershell stdout redirect)
288- TempXmlFile := ExpandConstant(' {tmp}\~taskxml.xml' );
289280 if not ExecWithResult(' powershell.exe' , ' -NoProfile "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value"' , ' ' , SW_HIDE, ewWaitUntilTerminated, ResultCode, UserSid) then
290281 begin
291282 Log(' RegisterAutoMountLogonTask: Failed to get user SID' );
@@ -294,7 +285,7 @@ begin
294285 UserSid := Trim(UserSid);
295286 Log(' RegisterAutoMountLogonTask: User SID = ' + UserSid);
296287
297- GvfsExe := ExpandConstant(' {app}\gvfs.exe' );
288+ GvfsExe := ExpandConstant(' {app}\Current\ gvfs.exe' );
298289 TaskHash := ' __TASK_HASH__' ; // Placeholder for drift detection; actual value computed by gvfs.exe
299290
300291 // Inline task XML matching LogonTaskRegistration.cs XmlTemplate
561552 SecureAppDataDir: string;
562553begin
563554 CommonAppDataDir := ExpandConstant(' {commonappdata}\GVFS' );
564- SecureAppDataDir := ExpandConstant(' {app}\ProgramData' );
555+ SecureAppDataDir := ExpandConstant(' {app}\Current\ ProgramData' );
565556
566557 MigrateFile(CommonAppDataDir + ' \{#GVFSConfigFileName}' , SecureAppDataDir + ' \{#GVFSConfigFileName}' );
567558 MigrateFile(CommonAppDataDir + ' \{#ServiceName}\{#GVFSStatuscacheTokenFileName}' , SecureAppDataDir + ' \{#ServiceName}\{#GVFSStatuscacheTokenFileName}' );
@@ -705,6 +696,8 @@ begin
705696 end ;
706697 ssPostInstall:
707698 begin
699+ CreateOrUpdateCurrentJunction();
700+ GarbageCollectOldVersions();
708701 MigrateConfigAndStatusCacheFiles();
709702 WriteOnDiskVersion16CapableFile();
710703 end ;
@@ -722,9 +715,209 @@ begin
722715 usUninstall:
723716 begin
724717 UninstallService(' GVFS.Service' , False);
725- RemovePath(ExpandConstant(' {app}' ));
718+ RemovePath(ExpandConstant(' {app}\Current' ));
719+ end ;
720+ end ;
721+ end ;
722+
723+ procedure CreateOrUpdateCurrentJunction ();
724+ var
725+ AppDir: string;
726+ JunctionPath: string;
727+ VersionDir: string;
728+ ResultCode: integer;
729+ begin
730+ AppDir := ExpandConstant(' {app}' );
731+ JunctionPath := AppDir + ' \Current' ;
732+ VersionDir := AppDir + ' \Versions\{#MyAppInstallerVersion}' ;
733+
734+ Log(' [GVFS-INSTALL] CreateOrUpdateCurrentJunction: Target version = {#MyAppInstallerVersion}' );
735+
736+ // Remove existing junction if present
737+ if DirExists(JunctionPath) then
738+ begin
739+ Log(' [GVFS-INSTALL] CreateOrUpdateCurrentJunction: Removing existing Current junction' );
740+ Exec(ExpandConstant(' {cmd}' ), ' /C rmdir "' + JunctionPath + ' "' , ' ' , SW_HIDE, ewWaitUntilTerminated, ResultCode);
741+ end ;
742+
743+ // Create new junction: Current -> Versions\<version>
744+ Log(' [GVFS-INSTALL] CreateOrUpdateCurrentJunction: Creating junction -> ' + VersionDir);
745+ if not Exec(ExpandConstant(' {cmd}' ), ' /C mklink /J "' + JunctionPath + ' " "' + VersionDir + ' "' , ' ' , SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0 ) then
746+ begin
747+ Log(' [GVFS-INSTALL] CreateOrUpdateCurrentJunction: mklink /J failed with exit code ' + IntToStr(ResultCode));
748+ RaiseException(' Fatal: Could not create Current junction at ' + JunctionPath);
749+ end
750+ else
751+ begin
752+ Log(' [GVFS-INSTALL] CreateOrUpdateCurrentJunction: Junction created successfully' );
753+ end ;
754+ end ;
755+
756+ function GetFileVersion (FilePath: string): string;
757+ var
758+ VersionMS: Cardinal;
759+ VersionLS: Cardinal;
760+ begin
761+ Result := ' ' ;
762+ if GetVersionNumbers(FilePath, VersionMS, VersionLS) then
763+ begin
764+ Result := Format(' %d.%d.%d.%d' , [
765+ VersionMS shr 16 ,
766+ VersionMS and $FFFF,
767+ VersionLS shr 16 ,
768+ VersionLS and $FFFF
769+ ]);
770+ end ;
771+ end ;
772+
773+ function IsProcessRunningFromPath (PathPrefix: string): Boolean;
774+ var
775+ ResultCode: integer;
776+ PowerShellCmd: string;
777+ begin
778+ // PowerShell: check if any gvfs.mount process has a path starting with PathPrefix
779+ PowerShellCmd := Format(' -NoProfile "$procs = Get-Process gvfs.mount -ErrorAction SilentlyContinue; ' +
780+ ' if ($procs) { foreach ($p in $procs) { ' +
781+ ' try { if ($p.Path -like '' %s*'' ) { exit 10 } } catch {} } }; exit 0"' , [PathPrefix]);
782+
783+ if Exec(' powershell.exe' , PowerShellCmd, ' ' , SW_HIDE, ewWaitUntilTerminated, ResultCode) then
784+ begin
785+ Result := (ResultCode = 10 );
786+ end
787+ else
788+ begin
789+ Log(' [GVFS-INSTALL] IsProcessRunningFromPath: PowerShell query failed' );
790+ Result := False;
791+ end ;
792+ end ;
793+
794+ procedure GarbageCollectOldVersions ();
795+ var
796+ AppDir: string;
797+ VersionsDir: string;
798+ CurrentVersion: string;
799+ FlatGvfsExe: string;
800+ FlatVersion: string;
801+ FindRec: TFindRec;
802+ VersionDirs: array of string;
803+ VersionTimes: array of Int64;
804+ Count: integer;
805+ I, J: integer;
806+ TempStr: string;
807+ TempTime: Int64;
808+ VersionPath: string;
809+ CanDelete: Boolean;
810+ begin
811+ AppDir := ExpandConstant(' {app}' );
812+ VersionsDir := AppDir + ' \Versions' ;
813+ CurrentVersion := ' {#MyAppInstallerVersion}' ;
814+
815+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Current version = ' + CurrentVersion);
816+
817+ // First, check for flat-layout binaries at {app}\GVFS.exe
818+ FlatGvfsExe := AppDir + ' \GVFS.exe' ;
819+ if FileExists(FlatGvfsExe) then
820+ begin
821+ FlatVersion := GetFileVersion(FlatGvfsExe);
822+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Detected flat layout with version ' + FlatVersion);
823+
824+ // Check if any mounts are running from the flat install
825+ if IsProcessRunningFromPath(AppDir + ' \' ) then
826+ begin
827+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Mounts running from flat layout - leaving in place' );
828+ end
829+ else
830+ begin
831+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: No mounts running from flat layout - would migrate to Versions\' + FlatVersion);
832+ // For now, just log. Full migration logic can move files to Versions\<FlatVersion>.
833+ // Defer to avoid complexity in first PR.
834+ end ;
835+ end ;
836+
837+ // Enumerate version directories
838+ Count := 0 ;
839+ SetArrayLength(VersionDirs, 0 );
840+ SetArrayLength(VersionTimes, 0 );
841+
842+ if not DirExists(VersionsDir) then
843+ begin
844+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Versions directory does not exist' );
845+ exit;
846+ end ;
847+
848+ if FindFirst(VersionsDir + ' \*' , FindRec) then
849+ begin
850+ try
851+ repeat
852+ if (FindRec.Name <> ' .' ) and (FindRec.Name <> ' ..' ) and (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0 ) then
853+ begin
854+ // Skip the current version
855+ if FindRec.Name <> CurrentVersion then
856+ begin
857+ SetArrayLength(VersionDirs, Count + 1 );
858+ SetArrayLength(VersionTimes, Count + 1 );
859+ VersionDirs[Count] := FindRec.Name ;
860+ VersionTimes[Count] := FindRec.Time;
861+ Count := Count + 1 ;
862+ end ;
863+ end ;
864+ until not FindNext(FindRec);
865+ finally
866+ FindClose(FindRec);
726867 end ;
727868 end ;
869+
870+ if Count = 0 then
871+ begin
872+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: No old versions to clean up' );
873+ exit;
874+ end ;
875+
876+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Found ' + IntToStr(Count) + ' old version(s)' );
877+
878+ // Sort by time (bubble sort, oldest first)
879+ for I := 0 to Count - 2 do
880+ begin
881+ for J := I + 1 to Count - 1 do
882+ begin
883+ if VersionTimes[I] > VersionTimes[J] then
884+ begin
885+ TempTime := VersionTimes[I];
886+ VersionTimes[I] := VersionTimes[J];
887+ VersionTimes[J] := TempTime;
888+ TempStr := VersionDirs[I];
889+ VersionDirs[I] := VersionDirs[J];
890+ VersionDirs[J] := TempStr;
891+ end ;
892+ end ;
893+ end ;
894+
895+ // Keep the 1 most recent old version (index Count-1), delete the rest
896+ for I := 0 to Count - 2 do
897+ begin
898+ VersionPath := VersionsDir + ' \' + VersionDirs[I];
899+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Checking version ' + VersionDirs[I]);
900+
901+ // Check if any mounts are running from this version
902+ CanDelete := not IsProcessRunningFromPath(VersionPath + ' \' );
903+
904+ if CanDelete then
905+ begin
906+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Deleting old version ' + VersionDirs[I]);
907+ if DelTree(VersionPath, True, True, True) then
908+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Deleted ' + VersionPath)
909+ else
910+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Failed to delete ' + VersionPath);
911+ end
912+ else
913+ begin
914+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Version ' + VersionDirs[I] + ' has running mounts - skipping' );
915+ end ;
916+ end ;
917+
918+ // Log the most recent old version that we're keeping
919+ if Count > 0 then
920+ Log(' [GVFS-INSTALL] GarbageCollectOldVersions: Keeping most recent old version ' + VersionDirs[Count - 1 ]);
728921end ;
729922
730923function PrepareToInstall (var NeedsRestart: Boolean): String;
@@ -735,8 +928,8 @@ begin
735928 Result := ' ' ;
736929 SetNuGetFeedIfNecessary();
737930
738- // User-level install model: no service, no staging flow, no mount/unmount .
739- // Just ensure no GVFS processes are running so files can be replaced .
931+ // User-level install model: no service, no staging flow.
932+ // Just ensure no GVFS processes are holding locks on files we're replacing .
740933 Log(' PrepareToInstall: Checking for running GVFS processes' );
741934 if IsGVFSRunning() then
742935 begin
@@ -750,7 +943,7 @@ begin
750943 begin
751944 if not EnsureGvfsNotRunning() then
752945 begin
753- Result := ' Installation cancelled .' ;
946+ Result := ' Cannot continue until VFS for Git is unmounted .' ;
754947 exit;
755948 end ;
756949 end ;
0 commit comments