Skip to content

Commit 2396024

Browse files
trop[bot]MarshallOfSoundVerteDinde
authored
fix: preserve staged update dir when pruning orphaned updates on macOS (#50217)
* fix: preserve staged update dir when pruning orphaned update dirs on macOS The previous squirrel.mac patch cleaned up all staged update directories before starting a new download. This kept disk usage bounded but broke quitAndInstall() if called while a subsequent checkForUpdates() was in flight — the already-staged bundle would be deleted out from under it. This reworks the patch to read ShipItState.plist and preserve the directory it references, deleting only truly orphaned update.XXXXXXX directories. Disk footprint stays bounded (at most 2 dirs: staged + in-progress) and quitAndInstall() remains safe mid-check. Also adds test coverage for the quitAndInstall/checkForUpdates race and a triple-stack scenario where 3 updates arrive without a restart. Refs #50200 Co-authored-by: Samuel Attard <sattard@anthropic.com> * chore: update patches --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Samuel Attard <sattard@anthropic.com> Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
1 parent 6d29863 commit 2396024

8 files changed

Lines changed: 466 additions & 78 deletions

File tree

patches/squirrel.mac/.patches

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ refactor_use_non-deprecated_nskeyedarchiver_apis.patch
99
chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
1010
fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
1111
use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
12-
fix_clean_up_old_staged_updates_before_downloading_new_update.patch
12+
fix_clean_up_orphaned_staged_updates_before_downloading_new_update.patch

patches/squirrel.mac/fix_clean_up_old_staged_updates_before_downloading_new_update.patch

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Andy Locascio <loc@anthropic.com>
3+
Date: Tue, 6 Jan 2026 08:23:03 -0800
4+
Subject: fix: clean up orphaned staged updates before downloading new update
5+
6+
When checkForUpdates() is called while an update is already staged,
7+
Squirrel creates a new temporary directory for the download without
8+
cleaning up the old one. This can lead to significant disk usage if
9+
the app keeps checking for updates without restarting.
10+
11+
This change adds a pruneOrphanedUpdateDirectories step before creating
12+
a new temp directory. Unlike a blanket prune, this reads the current
13+
ShipItState.plist and preserves the directory it references, deleting
14+
only truly orphaned update directories. This keeps the on-disk
15+
footprint bounded (at most 2 dirs) while ensuring quitAndInstall
16+
remains safe to call even when a new check is in progress.
17+
18+
Refs https://github.com/electron/electron/issues/50200
19+
20+
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
21+
index d156616e81e6f25a3bded30e6216b8fc311f31bc..41856e5754228d33982db72f97f2ff241615a357 100644
22+
--- a/Squirrel/SQRLUpdater.m
23+
+++ b/Squirrel/SQRLUpdater.m
24+
@@ -543,11 +543,19 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
25+
#pragma mark File Management
26+
27+
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
28+
- return [[[RACSignal
29+
+ // Clean up any orphaned update directories before creating a new one.
30+
+ // This prevents disk usage from growing when checkForUpdates() is called
31+
+ // multiple times without the app restarting. The currently staged update
32+
+ // (referenced by ShipItState.plist) is always preserved so quitAndInstall
33+
+ // remains safe to call while a new check is in progress.
34+
+ return [[[[[self
35+
+ pruneOrphanedUpdateDirectories]
36+
+ ignoreValues]
37+
+ concat:[RACSignal
38+
defer:^{
39+
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
40+
return [directoryManager storageURL];
41+
- }]
42+
+ }]]
43+
flattenMap:^(NSURL *storageURL) {
44+
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
45+
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
46+
@@ -668,25 +676,68 @@ - (RACSignal *)pruneUpdateDirectories {
47+
return [directoryManager storageURL];
48+
}]
49+
flattenMap:^(NSURL *storageURL) {
50+
- NSFileManager *manager = [[NSFileManager alloc] init];
51+
- NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
52+
- NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
53+
- return YES;
54+
- }];
55+
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:nil];
56+
+ }]
57+
+ setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
58+
+}
59+
60+
- return [[enumerator.rac_sequence.signal
61+
- filter:^(NSURL *enumeratedURL) {
62+
- NSString *name = enumeratedURL.lastPathComponent;
63+
- return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
64+
- }]
65+
- doNext:^(NSURL *directoryURL) {
66+
- NSError *error = nil;
67+
- if (![manager removeItemAtURL:directoryURL error:&error]) {
68+
- NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
69+
- }
70+
+/// Lazily removes orphaned temporary directories upon subscription, always
71+
+/// preserving the directory currently referenced by ShipItState.plist so that
72+
+/// quitAndInstall remains safe to call mid-check.
73+
+///
74+
+/// Safe to call in any state. Sends each removed directory then completes on
75+
+/// an unspecified thread. Errors reading the staged request are swallowed
76+
+/// (treated as "nothing staged").
77+
+- (RACSignal *)pruneOrphanedUpdateDirectories {
78+
+ return [[[[[SQRLShipItRequest
79+
+ readUsingURL:self.shipItStateURL]
80+
+ map:^(SQRLShipItRequest *request) {
81+
+ // The request holds the URL to the staged .app bundle; its parent
82+
+ // is the update.XXXXXXX directory we must preserve.
83+
+ return [request.updateBundleURL URLByDeletingLastPathComponent];
84+
+ }]
85+
+ catch:^(NSError *error) {
86+
+ // No staged request (or unreadable) — nothing to preserve.
87+
+ return [RACSignal return:nil];
88+
+ }]
89+
+ flattenMap:^(NSURL *stagedDirectoryURL) {
90+
+ SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
91+
+ return [[directoryManager storageURL]
92+
+ flattenMap:^(NSURL *storageURL) {
93+
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:stagedDirectoryURL];
94+
}];
95+
}]
96+
- setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
97+
+ setNameWithFormat:@"%@ -pruneOrphanedUpdateDirectories", self];
98+
+}
99+
+
100+
+/// Shared enumerate-and-delete logic for update temp directories.
101+
+///
102+
+/// storageURL - The Squirrel storage root to enumerate. Must not be nil.
103+
+/// excludedURL - Directory to skip (compared by standardized path). May be nil.
104+
+- (RACSignal *)removeUpdateDirectoriesInStorageURL:(NSURL *)storageURL excludingURL:(NSURL *)excludedURL {
105+
+ NSParameterAssert(storageURL != nil);
106+
+
107+
+ NSFileManager *manager = [[NSFileManager alloc] init];
108+
+ NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
109+
+ NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
110+
+ return YES;
111+
+ }];
112+
+
113+
+ NSString *excludedPath = excludedURL.URLByStandardizingPath.path;
114+
+
115+
+ return [[enumerator.rac_sequence.signal
116+
+ filter:^(NSURL *enumeratedURL) {
117+
+ NSString *name = enumeratedURL.lastPathComponent;
118+
+ if (![name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix]) return NO;
119+
+ if (excludedPath != nil && [enumeratedURL.URLByStandardizingPath.path isEqualToString:excludedPath]) return NO;
120+
+ return YES;
121+
+ }]
122+
+ doNext:^(NSURL *directoryURL) {
123+
+ NSError *error = nil;
124+
+ if (![manager removeItemAtURL:directoryURL error:&error]) {
125+
+ NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
126+
+ }
127+
+ }];
128+
}
129+
130+

0 commit comments

Comments
 (0)