TemporaryDirectoryRoot

This commit is contained in:
Kabir Oberai
2025-07-27 17:57:17 -04:00
parent bbcce89292
commit 368ec00c8e
4 changed files with 92 additions and 42 deletions

View File

@@ -214,7 +214,7 @@ public struct Planner: Sendable {
}
private func dumpDependencies() async throws -> PackageDependency {
let tempDir = try TemporaryDirectory(name: "xtool-dump-\(UUID().uuidString)")
let tempDir = try TemporaryDirectory(name: "xtool-dump")
let tempFileURL = tempDir.url.appendingPathComponent("dump.json")
// SwiftPM sometimes prints extraneous data to stdout, so ask

View File

@@ -2,6 +2,7 @@ import Foundation
import SwiftyMobileDevice
import ConcurrencyExtras
import Dependencies
import XUtils
extension LockdownClient {
static let installerLabel = "xtool"
@@ -54,9 +55,6 @@ public actor IntegratedInstaller {
private var appInstaller: AppInstaller?
private let tempDir = FileManager.default.temporaryDirectoryShim
.appendingPathComponent("sh.xtool.Staging")
private var stage: String?
private nonisolated let updateTask = LockIsolated<Task<Void, Never>?>(nil)
@@ -206,12 +204,9 @@ public actor IntegratedInstaller {
public func install(app: URL) async throws -> String {
try await self.updateStage(to: "Unpacking app", initialProgress: nil)
if FileManager.default.fileExists(atPath: tempDir.path) {
try? FileManager.default.removeItem(at: tempDir)
}
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let _tempDir = try TemporaryDirectory(name: "staging")
let tempDir = _tempDir.url
defer { withExtendedLifetime(_tempDir) {} }
switch app.pathExtension {
case "ipa":
@@ -235,7 +230,7 @@ public actor IntegratedInstaller {
try await self.updateProgress(to: 1)
let payload = self.tempDir.appendingPathComponent("Payload")
let payload = tempDir.appendingPathComponent("Payload")
guard let appDir = payload.implicitContents.first(where: { $0.pathExtension == "app" })
else { throw Error.appExtractionFailed }

View File

@@ -1,36 +1,5 @@
import Foundation
package struct TemporaryDirectory: ~Copyable {
private var shouldDelete = true
package let url: URL
package init(name: String) throws {
self.url = FileManager.default.temporaryDirectory.appendingPathComponent(name, isDirectory: true)
_delete()
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
}
private func _delete() {
try? FileManager.default.removeItem(at: url)
}
package consuming func persist() -> URL {
shouldDelete = false
return url
}
package consuming func persist(at location: URL) throws {
try FileManager.default.moveItem(at: url, to: location)
// we do this after moving, so that if the move fails we clean up
shouldDelete = false
}
deinit {
if shouldDelete { _delete() }
}
}
extension Data {
// AsyncBytes is Darwin-only :/

View File

@@ -0,0 +1,86 @@
import Foundation
package struct TemporaryDirectory: ~Copyable {
private static let debugTmp = ProcessInfo.processInfo.environment["XTL_DEBUG_TMP"] != nil
private var shouldDelete: Bool
package let url: URL
package init(name: String) throws {
do {
let basename = name.replacingOccurrences(of: ".", with: "_")
self.url = try TemporaryDirectoryRoot.shared.url
// ensures uniqueness
.appendingPathComponent("tmp-\(basename)-\(UUID().uuidString)")
.appendingPathComponent(name, isDirectory: true)
self.shouldDelete = true
} catch {
// non-copyable types can't be partially initialized so we need a stub value
self.url = URL(fileURLWithPath: "")
self.shouldDelete = false
throw error
}
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
if Self.debugTmp {
stderrPrint("Created TemporaryDirectory: \(url.path)")
}
}
private func _delete() {
guard !Self.debugTmp else { return }
try? FileManager.default.removeItem(at: url)
}
package consuming func persist(at location: URL) throws {
try FileManager.default.moveItem(at: url, to: location)
// we do this after moving, so that if the move fails we clean up
shouldDelete = false
}
deinit {
if shouldDelete { _delete() }
}
}
private struct TemporaryDirectoryRoot {
static let shared = TemporaryDirectoryRoot()
private let _url: Result<URL, Errors>
var url: URL {
get throws(Errors) {
try _url.get()
}
}
private init() {
let base: URL
let env = ProcessInfo.processInfo.environment
if let tmpdir = env["XTL_TMPDIR"] ?? env["TMPDIR"] {
base = URL(fileURLWithPath: tmpdir)
} else {
base = FileManager.default.temporaryDirectory
}
let url = base.appendingPathComponent("sh.xtool")
try? FileManager.default.removeItem(at: url)
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} catch {
self._url = .failure(Errors.tmpdirCreationFailed(url, error))
return
}
self._url = .success(url)
}
enum Errors: Error, CustomStringConvertible {
case tmpdirCreationFailed(URL, Error)
var description: String {
switch self {
case let .tmpdirCreationFailed(url, error):
"Could not create temporary directory at '\(url.path)': \(error)"
}
}
}
}