diff --git a/apps/helper/Sources/SimEnclaveHelperKit/InjectionEnv.swift b/apps/helper/Sources/SimEnclaveHelperKit/InjectionEnv.swift index a7a74f8..33a656f 100644 --- a/apps/helper/Sources/SimEnclaveHelperKit/InjectionEnv.swift +++ b/apps/helper/Sources/SimEnclaveHelperKit/InjectionEnv.swift @@ -7,19 +7,29 @@ /// `SIMBLE_*`) and are set and cleared directly. public enum InjectionEnv { /// Add `dylib` to a `DYLD_INSERT_LIBRARIES` value exactly once, preserving every other entry. + /// An entry already carrying `dylib`'s file name is replaced, so re-arming with a relocated + /// slice path never doubles our entry. public static func composed(current: String?, adding dylib: String) -> String { - var entries = split(current) - entries.removeAll { $0 == dylib } + let name = fileName(dylib) + var entries = split(current).filter { fileName($0) != name } entries.append(dylib) return entries.joined(separator: ":") } /// Remove `dylib` from a `DYLD_INSERT_LIBRARIES` value, leaving every other tool's entry. + /// Matches on the file name, so teardown finds our slice even when its absolute path has since + /// moved or the file is gone and only its canonical name is known. public static func removed(current: String?, removing dylib: String) -> String { - split(current).filter { $0 != dylib }.joined(separator: ":") + let name = fileName(dylib) + return split(current).filter { fileName($0) != name }.joined(separator: ":") } private static func split(_ value: String?) -> [String] { (value ?? "").split(separator: ":").map(String.init).filter { !$0.isEmpty } } + + /// The last path component of an insert-list entry, the slice's file name. + private static func fileName(_ path: String) -> String { + String(path.split(separator: "/").last ?? Substring(path)) + } } diff --git a/apps/helper/Sources/simenclave-menubar/HelperModel.swift b/apps/helper/Sources/simenclave-menubar/HelperModel.swift index 0e05dc8..9d51389 100644 --- a/apps/helper/Sources/simenclave-menubar/HelperModel.swift +++ b/apps/helper/Sources/simenclave-menubar/HelperModel.swift @@ -254,14 +254,14 @@ final class HelperModel { /// cleanup before the helper goes away and a stale port/token cannot mislead a later app. func clearInjection() { for sim in Self.bootedSimulators() { - // Remove only our slice from the shared DYLD list; never blanket-unset it, which would - // also drop a peer tool's interposer. Unset the variable only when nothing else is left. - if let dylib = Self.interposerDylib(for: sim.platform) { - let current = Self.simulatorEnv(sim.udid, "DYLD_INSERT_LIBRARIES") - let remaining = InjectionEnv.removed(current: current, removing: dylib) + // Remove our slice by its canonical file name, not a locator-resolved path: teardown + // must succeed even when the slice moved or was deleted since arming. Write the shared + // list only when it carries our entry, leaving a peer tool's interposer alone. + if let current = Self.simulatorEnv(sim.udid, "DYLD_INSERT_LIBRARIES") { + let remaining = InjectionEnv.removed(current: current, removing: sim.platform.dylibName) if remaining.isEmpty { Self.runSimctl(["spawn", sim.udid, "launchctl", "unsetenv", "DYLD_INSERT_LIBRARIES"]) - } else if remaining != (current ?? "") { + } else if remaining != current { Self.runSimctl(["spawn", sim.udid, "launchctl", "setenv", "DYLD_INSERT_LIBRARIES", remaining]) } } diff --git a/apps/helper/Tests/SimEnclaveHelperKitTests/InjectionEnvTests.swift b/apps/helper/Tests/SimEnclaveHelperKitTests/InjectionEnvTests.swift index 083b5c1..68eac70 100644 --- a/apps/helper/Tests/SimEnclaveHelperKitTests/InjectionEnvTests.swift +++ b/apps/helper/Tests/SimEnclaveHelperKitTests/InjectionEnvTests.swift @@ -41,4 +41,22 @@ final class InjectionEnvTests: XCTestCase { XCTAssertEqual(InjectionEnv.removed(current: other, removing: mine), other) XCTAssertEqual(InjectionEnv.removed(current: nil, removing: mine), "") } + + func testComposedDedupesByFileNameAcrossPaths() { + // Re-arm from a relocated build: the old path drops by name, the new one lands, no double. + let oldPath = "/old/simenclave-interpose.dylib" + let newPath = "/new/simenclave-interpose.dylib" + XCTAssertEqual(InjectionEnv.composed(current: oldPath, adding: newPath), newPath) + XCTAssertEqual( + InjectionEnv.composed(current: "\(other):\(oldPath)", adding: newPath), + "\(other):\(newPath)") + } + + func testRemovedMatchesByFileNameAcrossPaths() { + // Locator-independent teardown: remove by canonical name even when the armed path differs. + let armed = "/old/simenclave-interpose.dylib" + let name = "simenclave-interpose.dylib" + XCTAssertEqual(InjectionEnv.removed(current: armed, removing: name), "") + XCTAssertEqual(InjectionEnv.removed(current: "\(other):\(armed)", removing: name), other) + } }