|
| 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