Skip to content

Commit b3af219

Browse files
committed
Installer: switch to versioned folder layout with Current junction
Replaces flat {app}\ deployment with versioned Versions\<ver>\ + Current junction. Eliminates PendingUpgrade staging flow — new version goes to new folder, junction swaps atomically, mounts continue running from old version until unmounted. Changes: - [Files]: Deploy to {app}\Versions\{version}\ instead of {app}\ - [Registry]: PATH points to {app}\Current (junction) instead of {app}\ - [Dirs]: ProgramData under versioned folder - [Code]: Add CreateOrUpdateCurrentJunction() — creates/updates junction post-install - [Code]: Add GarbageCollectOldVersions() — keeps 1 most recent old version, deletes older (skips versions with running mounts detected via Get-Process gvfs.mount | .Path) - [Code]: Remove KeepMountsRunning variable, IsNormalInstall/IsStagingInstall checks - [Code]: Remove StagingUpdateService, ShowMountChoiceDialog (no longer needed) - [Code]: Simplify PrepareToInstall — no mount detection, no staging, just stop service - [Code]: Update InstallGVFSService to reference {app}\Current\GVFS.Service.exe - [Code]: Update MountRepos, MigrateConfigAndStatusCacheFiles, WriteOnDiskVersion16CapableFile to use Current junction paths - CurStepChanged: Remove staging logic, call CreateOrUpdateCurrentJunction + GarbageCollectOldVersions - CurUninstallStepChanged: Remove {app}\Current from PATH instead of {app} Flat-layout migration stub added (detects {app}\GVFS.exe, logs version) but defers actual file move to future PR to reduce complexity. Assisted-by: Claude Sonnet 4.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 141d39a commit b3af219

1 file changed

Lines changed: 213 additions & 20 deletions

File tree

GVFS/GVFS.Installers/Setup.iss

Lines changed: 213 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ Name: "full"; Description: "Full installation"; Flags: iscustom;
5959
Type: 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]
7473
Root: 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

7877
Root: HKLM; Subkey: "{#FileSystemKey}"; \
7978
ValueType: dword; ValueName: "NtfsEnableDetailedCleanupResults"; ValueData: "1"; \
@@ -254,7 +253,7 @@ procedure WriteOnDiskVersion16CapableFile();
254253
var
255254
FilePath: string;
256255
begin
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
@@ -561,7 +552,7 @@ var
561552
SecureAppDataDir: string;
562553
begin
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]);
728921
end;
729922
730923
function 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

Comments
 (0)