diff --git a/HelperXPCShared/FileOperations.swift b/HelperXPCShared/FileOperations.swift new file mode 100644 index 00000000..e0937581 --- /dev/null +++ b/HelperXPCShared/FileOperations.swift @@ -0,0 +1,61 @@ +import Foundation +import os.log + +enum FileOperations { + private static let subsystem = Bundle.main.bundleIdentifier! + static let fileOperations = Logger(subsystem: subsystem, category: "fileOperations") + + static func moveApp(at source: String, to destination: String, completion: @escaping ((any Error)?) -> Void) { + do { + guard URL(fileURLWithPath: source).hasDirectoryPath else { throw XPCDelegateError(.invalidSourcePath)} + + guard URL(fileURLWithPath: destination).deletingLastPathComponent().hasDirectoryPath else { throw + XPCDelegateError(.invalidDestinationPath)} + + try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination)) + completion(nil) + } catch { + completion(error) + } + } + + // does an Xcode.app file exist? + static func createSymbolicLink(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) { + do { + if FileManager.default.fileExists(atPath: destination) { + let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destination) + + if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink { + try FileManager.default.removeItem(atPath: destination) + Self.fileOperations.info("Successfully deleted old symlink") + } else { + throw XPCDelegateError(.destinationIsNotASymbolicLink) + } + } + + try FileManager.default.createSymbolicLink(atPath: destination, withDestinationPath: source) + Self.fileOperations.info("Successfully created symbolic link with \(destination)") + completion(nil) + } catch { + completion(error) + } + } + + static func rename(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) { + do { + try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination)) + completion(nil) + } catch { + completion(error) + } + } + + static func remove(path: String, completion: @escaping ((any Error)?) -> Void) { + do { + try FileManager.default.removeItem(atPath: path) + completion(nil) + } catch { + completion(error) + } + } +} diff --git a/HelperXPCShared/HelperXPCShared.swift b/HelperXPCShared/HelperXPCShared.swift index d72d7be9..b5a6613e 100644 --- a/HelperXPCShared/HelperXPCShared.swift +++ b/HelperXPCShared/HelperXPCShared.swift @@ -12,4 +12,54 @@ protocol HelperXPCProtocol: Sendable { func addStaffToDevelopersGroup(completion: @escaping (Error?) -> Void) func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void) func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void) + func moveApp(at source: String, to destination: String, completion: @escaping (Error?) -> Void) + func createSymbolicLink(source: String, destination: String, completion: @escaping (Error?) -> Void) + func rename(source: String, destination: String, completion: @escaping (Error?) -> Void) + func remove(path: String, completion: @escaping (Error?) -> Void) +} + +struct XPCDelegateError: CustomNSError { + enum Code: Int { + case invalidXcodePath + case invalidSourcePath + case invalidDestinationPath + case destinationIsNotASymbolicLink + } + + let code: Code + + init(_ code: Code) { + self.code = code + } + + // MARK: - CustomNSError + + static var errorDomain: String { "XPCDelegateError" } + + var errorCode: Int { code.rawValue } + + var errorUserInfo: [String : Any] { + switch code { + case .invalidXcodePath: + return [ + NSLocalizedDescriptionKey: "Invalid Xcode path.", + NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute." + ] + case .invalidSourcePath: + return [ + NSLocalizedDescriptionKey: "Invalid source path.", + NSLocalizedFailureReasonErrorKey: "Source path must be absolute and must be a directory." + ] + case .invalidDestinationPath: + return [ + NSLocalizedDescriptionKey: "Invalid destination path.", + NSLocalizedFailureReasonErrorKey: "Destination path must be absolute and must be a directory." + ] + case .destinationIsNotASymbolicLink: + return [ + NSLocalizedDescriptionKey: "Invalid destination path.", + NSLocalizedFailureReasonErrorKey: "Destination path must be a symbolic link." + ] + } + } } diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 05f49654..516f31c0 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 14d2f5a1273f6c350cad4406 /* NewVersionNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884f01aed2f43048ab4d3323 /* NewVersionNotificationTests.swift */; }; + 1596C2913043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; }; + 1596C2923043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; }; 15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; }; 3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; }; 332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; }; @@ -185,6 +187,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1596C2903043765600178C86 /* FileOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperations.swift; sourceTree = ""; }; 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; }; 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = ""; }; 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = ""; }; @@ -432,6 +435,7 @@ isa = PBXGroup; children = ( CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */, + 1596C2903043765600178C86 /* FileOperations.swift */, ); path = HelperXPCShared; sourceTree = ""; @@ -863,6 +867,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1596C2913043765600178C86 /* FileOperations.swift in Sources */, CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */, CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */, CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */, @@ -911,6 +916,7 @@ 332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */, CA61A6E0259835580008926E /* Xcode.swift in Sources */, CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */, + 1596C2923043765600178C86 /* FileOperations.swift in Sources */, CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */, CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4f9d80c0..5445e6ff 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/mxcl/LegibleError", "state": { "branch": null, - "revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710", - "version": "1.0.4" + "revision": "bc596702d7ff618c3f90ba480eeb48b3e83a2fbe", + "version": "1.0.6" } }, { @@ -60,17 +60,17 @@ "repositoryURL": "https://github.com/mxcl/Path.swift", "state": { "branch": null, - "revision": "8e355c28e9393c42e58b18c54cace2c42c98a616", - "version": "1.4.1" + "revision": "74ec90bbe50a3376e399286fed48b60db9b91bb1", + "version": "1.6.0" } }, { "package": "Sparkle", - "repositoryURL": "https://github.com/sparkle-project/Sparkle/", + "repositoryURL": "https://github.com/sparkle-project/Sparkle", "state": { "branch": null, - "revision": "0ef1ee0220239b3776f433314515fd849025673f", - "version": "2.6.4" + "revision": "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a", + "version": "2.9.6" } }, { @@ -78,8 +78,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version": "1.0.5" + "revision": "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version": "1.6.0" } }, { diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 8d8a2f32..02850d70 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -254,7 +254,14 @@ extension AppState { XcodeUnarchiveService( unarchive: { _ = try await self.unxipOrUnxipExperimentAsync($0) }, fileExists: { path in Current.files.fileExists(atPath: path) }, - moveItem: { source, destination in try Current.files.moveItem(at: source, to: destination) }, + moveItem: { source, destination in + if Current.helper.usePrivilegedHelperForFileOperations { + try await self.installHelperIfNecessaryAsync() + try await Current.helper.moveAppAsync(source.path, destination.path) + } else { + try Current.files.moveItem(at: source, to: destination) + } + }, removeItem: { url in try Current.files.removeItem(at: url) } ) } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 7924e649..f0ee0729 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -29,6 +29,7 @@ enum PreferenceKey: String { case enableGroupedXcodeList case expandedMajorXcodeVersions case expandedMinorXcodeVersions + case usePrivilegeHelperForFileOperations func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) } } @@ -147,6 +148,12 @@ class AppState: ObservableObject { var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() } + @Published var usePrivilegedHelperForFileOperations = false { + didSet { + Current.defaults.set(usePrivilegedHelperForFileOperations, forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue) + } + } + @Published var showOpenInRosettaOption = false { didSet { Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption") @@ -263,6 +270,7 @@ class AppState: ObservableObject { showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false enableGroupedXcodeList = Current.defaults.get(forKey: PreferenceKey.enableGroupedXcodeList.rawValue) as? Bool ?? true + usePrivilegedHelperForFileOperations = Current.defaults.bool(forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue) ?? false } // MARK: Timer @@ -759,14 +767,9 @@ class AppState: ObservableObject { } guard - var installedXcodePath = xcode.installedPath + let installedXcodePath = xcode.installedPath else { return } - if onSelectActionType == .rename { - guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return } - installedXcodePath = newDestinationXcodePath - } - selectTask?.cancel() let taskID = UUID() selectTaskID = taskID @@ -778,13 +781,20 @@ class AppState: ObservableObject { } } do { + var installedXcodePath = installedXcodePath try await installHelperIfNecessaryAsync() try Task.checkCancellation() + + if onSelectActionType == .rename { + guard let newDestinationXcodePath = await renameToXcode(xcode: xcode) else { return } + installedXcodePath = newDestinationXcodePath + } + try await Current.helper.switchXcodePathAsync(installedXcodePath.string) try Task.checkCancellation() await updateSelectedXcodePathAsync() if createSymLinkOnSelect && onSelectActionType != .rename { - createSymbolicLink(to: installedXcodePath) + await createSymbolicLink(to: installedXcodePath) } } catch is CancellationError { } catch { @@ -826,25 +836,39 @@ class AppState: ObservableObject { func createSymbolicLink(xcode: Xcode, isBeta: Bool = false) { guard let installedXcodePath = xcode.installedPath else { return } - createSymbolicLink(to: installedXcodePath, isBeta: isBeta) + Task { @MainActor in + await createSymbolicLink(to: installedXcodePath, isBeta: isBeta) + } } - func createSymbolicLink(to installedXcodePath: Path, isBeta: Bool = false) { + func createSymbolicLink(to installedXcodePath: Path, isBeta: Bool = false) async { let destinationPath = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app" do { - let service = XcodeSelectionFilesystemService( - installedXcode: { Current.files.installedXcode(destination: $0) } - ) - let result = try service.createSymbolicLink( - to: installedXcodePath, - in: Path.installDirectory, - isBeta: isBeta - ) - if result.replacedExistingSymlink { - Logger.appState.info("Successfully deleted old symlink") + if Current.helper.usePrivilegedHelperForFileOperations { + if Current.files.fileExists(atPath: destinationPath.string) { + let attributes = try FileManager.default.attributesOfItem(atPath: destinationPath.string) + guard attributes[.type] as? FileAttributeType == .typeSymbolicLink else { + throw XcodeSelectionFilesystemError.destinationExistsAndIsNotSymlink(destinationPath) + } + } + // The helper's createSymbolicLink deletes an existing symlink at the destination before creating the new one. + try await Current.helper.createSymbolicLinkAsync(installedXcodePath.string, destinationPath.string) + Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app") + } else { + let service = XcodeSelectionFilesystemService( + installedXcode: { Current.files.installedXcode(destination: $0) } + ) + let result = try service.createSymbolicLink( + to: installedXcodePath, + in: Path.installDirectory, + isBeta: isBeta + ) + if result.replacedExistingSymlink { + Logger.appState.info("Successfully deleted old symlink") + } + Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app") } - Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app") } catch { Logger.appState.error("Unable to create symbolic Link") self.error = error @@ -855,19 +879,31 @@ class AppState: ObservableObject { } } - func renameToXcode(xcode: Xcode) -> Path? { + func renameToXcode(xcode: Xcode) async -> Path? { guard let installedXcodePath = xcode.installedPath else { return nil } do { - let service = XcodeSelectionFilesystemService( - installedXcode: { Current.files.installedXcode(destination: $0) } - ) - let renamedPath = try service.renameForSelection( - installedXcodePath: installedXcodePath, - in: Path.installDirectory - ) - Logger.appState.debug("Renamed selected Xcode to Xcode.app") - return renamedPath + if Current.helper.usePrivilegedHelperForFileOperations { + let destinationPath = Path.installDirectory/"Xcode.app" + if Current.files.fileExists(atPath: destinationPath.string), + let originalXcode = Current.files.installedXcode(destination: destinationPath) { + let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app" + try await Current.helper.renameAsync(destinationPath.string, "\(Path.installDirectory)/\(newName)") + } + try await Current.helper.renameAsync(installedXcodePath.string, destinationPath.string) + Logger.appState.debug("Renamed selected Xcode to Xcode.app") + return destinationPath + } else { + let service = XcodeSelectionFilesystemService( + installedXcode: { Current.files.installedXcode(destination: $0) } + ) + let renamedPath = try service.renameForSelection( + installedXcodePath: installedXcodePath, + in: Path.installDirectory + ) + Logger.appState.debug("Renamed selected Xcode to Xcode.app") + return renamedPath + } } catch { Logger.appState.error("Unable to create rename Xcode.app back to original") self.error = error @@ -921,10 +957,16 @@ class AppState: ObservableObject { ) else { throw FileError.fileNotFound(path.string) } - _ = try XcodeUninstallService( - removeItem: { url in try Current.files.removeItem(at: url) }, - trashItem: { url in try Current.files.trashItem(at: url) } - ).uninstall(xcode, emptyTrash: false) + + if Current.helper.usePrivilegedHelperForFileOperations { + try await installHelperIfNecessaryAsync() + try await Current.helper.removeAsync(xcode.path.string) + } else { + _ = try XcodeUninstallService( + removeItem: { url in try Current.files.removeItem(at: url) }, + trashItem: { url in try Current.files.trashItem(at: url) } + ).uninstall(xcode, emptyTrash: false) + } } private func waitForAuthenticationTerminalState() async throws { diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 44a1f92a..4373d470 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -285,4 +285,11 @@ public struct Helper: Sendable { var addStaffToDevelopersGroupAsync: @Sendable () async throws -> Void = { try await helperClient.addStaffToDevelopersGroupAsync() } var acceptXcodeLicenseAsync: @Sendable (_ absoluteXcodePath: String) async throws -> Void = { try await helperClient.acceptXcodeLicenseAsync(absoluteXcodePath: $0) } var runFirstLaunchAsync: @Sendable (_ absoluteXcodePath: String) async throws -> Void = { try await helperClient.runFirstLaunchAsync(absoluteXcodePath: $0) } + var moveAppAsync: @Sendable (_ source: String, _ destination: String) async throws -> Void = { try await helperClient.moveAppAsync(at: $0, to: $1) } + var createSymbolicLinkAsync: @Sendable (_ source: String, _ destination: String) async throws -> Void = { try await helperClient.createSymbolicLinkAsync(source: $0, destination: $1) } + var renameAsync: @Sendable (_ source: String, _ destination: String) async throws -> Void = { try await helperClient.renameAsync(source: $0, destination: $1) } + var removeAsync: @Sendable (_ path: String) async throws -> Void = { try await helperClient.removeAsync(path: $0) } + var usePrivilegedHelperForFileOperations: Bool { + Current.defaults.bool(forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue) ?? false + } } diff --git a/Xcodes/Backend/HelperClient.swift b/Xcodes/Backend/HelperClient.swift index f7f8df2e..1fb73d8b 100644 --- a/Xcodes/Backend/HelperClient.swift +++ b/Xcodes/Backend/HelperClient.swift @@ -119,6 +119,86 @@ final class HelperClient { Logger.helperClient.info("\(#function): finished") } + func moveAppAsync(at source: String, to destination: String) async throws { + Logger.helperClient.info(#function) + + guard Current.helper.usePrivilegedHelperForFileOperations else { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + FileOperations.moveApp(at: source, to: destination) { error in + if let error { continuation.resume(throwing: error) } else { continuation.resume() } + } + } + return + } + + try await performVoidHelperRequest { helper, finish in + helper.moveApp(at: source, to: destination) { possibleError in + finish(possibleError.map(Result.failure) ?? .success(())) + } + } + Logger.helperClient.info("\(#function): finished") + } + + func createSymbolicLinkAsync(source: String, destination: String) async throws { + Logger.helperClient.info(#function) + + guard Current.helper.usePrivilegedHelperForFileOperations else { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + FileOperations.createSymbolicLink(source: source, destination: destination) { error in + if let error { continuation.resume(throwing: error) } else { continuation.resume() } + } + } + return + } + + try await performVoidHelperRequest { helper, finish in + helper.createSymbolicLink(source: source, destination: destination) { possibleError in + finish(possibleError.map(Result.failure) ?? .success(())) + } + } + Logger.helperClient.info("\(#function): finished") + } + + func renameAsync(source: String, destination: String) async throws { + Logger.helperClient.info(#function) + + guard Current.helper.usePrivilegedHelperForFileOperations else { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + FileOperations.rename(source: source, destination: destination) { error in + if let error { continuation.resume(throwing: error) } else { continuation.resume() } + } + } + return + } + + try await performVoidHelperRequest { helper, finish in + helper.rename(source: source, destination: destination) { possibleError in + finish(possibleError.map(Result.failure) ?? .success(())) + } + } + Logger.helperClient.info("\(#function): finished") + } + + func removeAsync(path: String) async throws { + Logger.helperClient.info(#function) + + guard Current.helper.usePrivilegedHelperForFileOperations else { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + FileOperations.remove(path: path) { error in + if let error { continuation.resume(throwing: error) } else { continuation.resume() } + } + } + return + } + + try await performVoidHelperRequest { helper, finish in + helper.remove(path: path) { possibleError in + finish(possibleError.map(Result.failure) ?? .success(())) + } + } + Logger.helperClient.info("\(#function): finished") + } + private func performVoidHelperRequest(_ operation: @escaping @Sendable (HelperXPCProtocol, @escaping @Sendable (Result) -> Void) -> Void) async throws { try await performHelperRequest(operation) } diff --git a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift index c938bb3e..201b1b93 100644 --- a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift @@ -149,7 +149,10 @@ struct AdvancedPreferencePane: View { .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) - + + Toggle("UsePrivilegedHelperForFileOperations", isOn: $appState.usePrivilegedHelperForFileOperations) + .disabled(PreferenceKey.usePrivilegeHelperForFileOperations.isManaged()) + Spacer() } } diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 93e638da..cad3415a 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -25567,6 +25567,124 @@ } } }, + "UsePrivilegedHelperForFileOperations" : { + "localizations" : { + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Realitza operacions de fitxers mitjançant l'ajudant privilegiat" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dateivorgänge über den privilegierten Helfer ausführen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Εκτέλεση λειτουργιών αρχείων μέσω του προνομιούχου βοηθού" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perform file operations using Privileged Helper" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Realizar operaciones de archivos mediante el ayudante con privilegios" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suorita tiedostotoiminnot etuoikeutetun apuohjelman kautta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effectuer les opérations sur les fichiers via l'assistant privilégié" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "विशेषाधिकार प्राप्त हेल्पर का उपयोग करके फ़ाइल संचालन करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esegui operazioni sui file tramite l'helper privilegiato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特権ヘルパーを使用してファイル操作を実行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "권한이 부여된 헬퍼를 사용하여 파일 작업 수행" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestandsbewerkingen uitvoeren via de geprivilegieerde helper" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykonuj operacje na plikach za pomocą uprzywilejowanego pomocnika" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Executar operações de arquivo usando o Auxiliar Privilegiado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выполнять файловые операции с помощью привилегированного помощника" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dosya işlemlerini Yetkili Yardımcı ile gerçekleştir" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виконувати файлові операції за допомогою привілейованого помічника" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用特权助手执行文件操作" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用特權輔助程式執行檔案操作" + } + } + } + }, "UseUnxipExperiment" : { "localizations" : { "ar" : { diff --git a/XcodesTests/AppStateTests.swift b/XcodesTests/AppStateTests.swift index f1f0000a..712565df 100644 --- a/XcodesTests/AppStateTests.swift +++ b/XcodesTests/AppStateTests.swift @@ -114,7 +114,7 @@ class AppStateTests: XCTestCase { XCTAssertNil(subject.presentedAlert) } - func test_CreateSymbolicLink_UsesProvidedInstalledPath() throws { + func test_CreateSymbolicLink_UsesProvidedInstalledPath() async throws { let installDirectory = try XCTUnwrap(Path( NSTemporaryDirectory() .appending("XcodesAppStateTests-") @@ -129,7 +129,7 @@ class AppStateTests: XCTestCase { key == "installPath" ? installDirectory.string : nil } - subject.createSymbolicLink(to: installedXcodePath) + await subject.createSymbolicLink(to: installedXcodePath) let destination = try FileManager.default.destinationOfSymbolicLink(atPath: symlinkPath.string) XCTAssertEqual(destination, installedXcodePath.string) diff --git a/com.xcodesorg.xcodesapp.Helper/XPCDelegate.swift b/com.xcodesorg.xcodesapp.Helper/XPCDelegate.swift index eb9fe069..2e5a4213 100644 --- a/com.xcodesorg.xcodesapp.Helper/XPCDelegate.swift +++ b/com.xcodesorg.xcodesapp.Helper/XPCDelegate.swift @@ -51,6 +51,26 @@ final class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol { func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void) { run(url: URL(fileURLWithPath: absoluteXcodePath + "/Contents/Developer/usr/bin/xcodebuild"), arguments: ["-runFirstLaunch"], completion: completion) } + + func moveApp(at source: String, to destination: String, completion: @escaping (Error?) -> Void) { + Logger.xpcDelegate.info("\(#function)") + FileOperations.moveApp(at: source, to: destination, completion: completion) + } + + func createSymbolicLink(source: String, destination: String, completion: @escaping (Error?) -> Void) { + Logger.xpcDelegate.info("\(#function)") + FileOperations.createSymbolicLink(source: source, destination: destination, completion: completion) + } + + func rename(source: String, destination: String, completion: @escaping (Error?) -> Void) { + Logger.xpcDelegate.info("\(#function)") + FileOperations.rename(source: source, destination: destination, completion: completion) + } + + func remove(path: String, completion: @escaping (Error?) -> Void) { + Logger.xpcDelegate.info("\(#function)") + FileOperations.remove(path: path, completion: completion) + } } // MARK: - Run @@ -69,34 +89,3 @@ private func run(url: URL, arguments: [String], completion: @escaping (Error?) - completion(error) } } - - -// MARK: - Errors - -struct XPCDelegateError: CustomNSError { - enum Code: Int { - case invalidXcodePath - } - - let code: Code - - init(_ code: Code) { - self.code = code - } - - // MARK: - CustomNSError - - static var errorDomain: String { "XPCDelegateError" } - - var errorCode: Int { code.rawValue } - - var errorUserInfo: [String : Any] { - switch code { - case .invalidXcodePath: - return [ - NSLocalizedDescriptionKey: "Invalid Xcode path.", - NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute." - ] - } - } -}