Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions assets/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,22 @@
}
}
},
"Couldn’t Uninstall SimStore": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Couldn’t Uninstall SimStore"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法卸载 SimStore"
}
}
}
},
"Couldn’t Update SimStore": {
"localizations": {
"en": {
Expand Down Expand Up @@ -1377,6 +1393,22 @@
}
}
},
"Move SimStore to a writable folder before uninstalling it.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Move SimStore to a writable folder before uninstalling it."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "请先将 SimStore 移到可写入的文件夹,再卸载。"
}
}
}
},
"Newer compatible versions of your installed apps will appear here.": {
"localizations": {
"en": {
Expand Down Expand Up @@ -2033,6 +2065,22 @@
}
}
},
"SimStore, its caches, settings, and temporary files will be permanently deleted. SimStore will then quit.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "SimStore, its caches, settings, and temporary files will be permanently deleted. SimStore will then quit."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "将永久删除 SimStore 及其缓存、设置和临时文件。随后 SimStore 将退出。"
}
}
}
},
"Simulator": {
"localizations": {
"en": {
Expand Down Expand Up @@ -2465,6 +2513,22 @@
}
}
},
"The running SimStore app could not be verified. Nothing was uninstalled.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The running SimStore app could not be verified. Nothing was uninstalled."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法验证当前运行的 SimStore,未执行卸载。"
}
}
}
},
"The simulator application could not be found. Check your Xcode installation.": {
"localizations": {
"en": {
Expand Down Expand Up @@ -2497,6 +2561,22 @@
}
}
},
"The uninstall assistant could not finish. Try again.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The uninstall assistant could not finish. Try again."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "卸载助手未能完成操作,请重试。"
}
}
}
},
"The update assistant could not start. SimStore has not been replaced.": {
"localizations": {
"en": {
Expand Down Expand Up @@ -2657,6 +2737,38 @@
}
}
},
"Uninstall SimStore?": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Uninstall SimStore?"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "卸载 SimStore?"
}
}
}
},
"Uninstall SimStore…": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Uninstall SimStore…"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "卸载 SimStore…"
}
}
}
},
"Uninstalling…": {
"localizations": {
"en": {
Expand Down
6 changes: 6 additions & 0 deletions docs/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ The client's own `version.json` feed is fetched on each explicit update check wi
Self-update archives use the same uncached download session as Simulator app installations. They are streamed to a private, randomly named `.simstore-update-*` directory beside the installed SimStore app, rather than a persistent download cache, so an atomic replacement can stay on one filesystem. The verified archive is deleted after preparation. The extracted bundle, copied assistant, and previous version remain only until the update succeeds or finishes failing/canceling. An interrupted process can leave a staging directory for manual cleanup.

If the update assistant encounters an error, it writes a translated message to `update-error.json` in the client's cache directory. The running or relaunched client consumes and removes that message when reporting the failure. This file is a one-time failure report, not a version or package cache; losing it does not affect the installed application.

## Uninstalling SimStore

SimStore → Uninstall SimStore… shows a confirmation with the running application's path. Uninstall removes that bundle, its catalog and icon caches, application support data, preferences including host-specific preferences, saved window state, HTTP storage, WebKit data, logs, and installation temporary directories belonging to its bundle identifier. It also removes abandoned uninstall staging directories and self-update staging directories beside the app; a staging request pointing to another application is preserved.

The existing native assistant runs from a private temporary directory and verifies the target process and bundle. It pauses the app before synchronizing and clearing its preference domains and deleting files, removes its own staging directory, then sends SIGKILL to the target PID. This prevents the app from recreating caches or saving window state during normal termination. Cleanup failure resumes the app and returns an error. Uninstall is unavailable while a SimStore update is in progress.
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ add_executable(SimStore MACOSX_BUNDLE
SelfUpdateStore.swift
SelfUpdateView.swift
SelfUpdateController.swift
SelfUninstallation.swift
SelfUninstaller.swift
SelfUninstallController.swift
)

add_executable(SimStoreUpdater
SelfUpdateHelper.swift
SelfUninstallation.swift
SelfUpdateInstallation.swift
SelfUpdateRelease.swift
SelfUpdatePackage.swift
Expand Down
53 changes: 53 additions & 0 deletions src/SelfUninstallController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import AppKit
import Observation

@MainActor
@Observable
final class SelfUninstallController {
private(set) var isRunning = false
@ObservationIgnored private let application: URL
@ObservationIgnored private let uninstall: @MainActor () async throws -> Void
@ObservationIgnored private let failureTitle = String(localized: "Couldn’t Uninstall SimStore")

init(
application: URL = Bundle.main.bundleURL,
uninstall: @escaping @MainActor () async throws -> Void = SelfUninstaller.start
) {
self.application = application
self.uninstall = uninstall
}

func confirm(beforeUninstall: () -> Void = {}) {
guard !isRunning else { return }
isRunning = true
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = String(localized: "Uninstall SimStore?")
alert.informativeText =
String(
localized:
"SimStore, its caches, settings, and temporary files will be permanently deleted. SimStore will then quit."
)
+ "\n\n" + application.path
alert.addButton(withTitle: String(localized: "Cancel")).keyEquivalent = "\u{1b}"
alert.addButton(withTitle: String(localized: "Uninstall")).hasDestructiveAction = true
guard alert.runModal() == .alertSecondButtonReturn else {
isRunning = false
return
}
beforeUninstall()
Task {
defer { isRunning = false }
do {
try await uninstall()
} catch {
let failure = NSAlert()
failure.alertStyle = .warning
failure.messageText = failureTitle
failure.informativeText = error.localizedDescription
failure.addButton(withTitle: String(localized: "OK"))
failure.runModal()
}
}
}
}
Loading