@@ -754,6 +754,7 @@ enum VSCodeCLILaunchConfigurationBuilder {
754754 }
755755 environment.removeValue(forKey: "NODE_OPTIONS")
756756 environment.removeValue(forKey: "NODE_REPL_EXTERNAL_MODULE")
757+ environment["VSCODE_CLI_USE_FILE_KEYRING"] = "1"
757758
758759 return VSCodeCLILaunchConfiguration(
759760 executableURL: codeTunnelURL,
@@ -928,7 +929,7 @@ final class VSCodeServeWebController {
928929 return (processes, tokenFileURLs, completions)
929930 }
930931
931- for tokenFileURL in tokenFileURLs {
932+ for tokenFileURL in tokenFileURLs where tokenFileURL.path.hasPrefix(NSTemporaryDirectory()) {
932933 Self.removeConnectionTokenFile(at: tokenFileURL)
933934 }
934935
@@ -966,11 +967,17 @@ final class VSCodeServeWebController {
966967
967968 let process = Process()
968969 process.executableURL = launchConfiguration.executableURL
970+ // TODO(#21): --port 0 means VS Code gets an OS-assigned ephemeral port each restart, so
971+ // the browser must re-navigate to the new URL. To stabilise: either use a fixed loopback
972+ // port, or persist the port assigned by VS Code (from ServeWebOutputCollector) under
973+ // vscodeServerDataDir and reuse it here. The --server-data-dir and persistent
974+ // connection-token already fix Settings Sync / OAuth auth; the URL change is UX-only.
969975 process.arguments = launchConfiguration.argumentsPrefix + [
970976 "serve-web",
971977 "--accept-server-license-terms",
972978 "--host", "127.0.0.1",
973979 "--port", "0",
980+ "--server-data-dir", Self.vscodeServerDataDir?.path ?? NSTemporaryDirectory(),
974981 "--connection-token-file", connectionTokenFileURL.path,
975982 ]
976983 process.environment = launchConfiguration.environment
@@ -1006,7 +1013,7 @@ final class VSCodeServeWebController {
10061013 }
10071014 if let tokenFileURL = self.connectionTokenFilesByProcessID.removeValue(
10081015 forKey: ObjectIdentifier(terminatedProcess)
1009- ) {
1016+ ), tokenFileURL.path.hasPrefix(NSTemporaryDirectory()) {
10101017 Self.removeConnectionTokenFile(at: tokenFileURL)
10111018 }
10121019 }
@@ -1028,7 +1035,7 @@ final class VSCodeServeWebController {
10281035 }
10291036 if let tokenFileURL = self.connectionTokenFilesByProcessID.removeValue(
10301037 forKey: ObjectIdentifier(process)
1031- ) {
1038+ ), tokenFileURL.path.hasPrefix(NSTemporaryDirectory()) {
10321039 Self.removeConnectionTokenFile(at: tokenFileURL)
10331040 }
10341041 return false
@@ -1037,7 +1044,9 @@ final class VSCodeServeWebController {
10371044 guard didStart else {
10381045 stdoutPipe.fileHandleForReading.readabilityHandler = nil
10391046 stderrPipe.fileHandleForReading.readabilityHandler = nil
1040- Self.removeConnectionTokenFile(at: connectionTokenFileURL)
1047+ if connectionTokenFileURL.path.hasPrefix(NSTemporaryDirectory()) {
1048+ Self.removeConnectionTokenFile(at: connectionTokenFileURL)
1049+ }
10411050 return nil
10421051 }
10431052
@@ -1058,7 +1067,7 @@ final class VSCodeServeWebController {
10581067 }
10591068 if let tokenFileURL = self.connectionTokenFilesByProcessID.removeValue(
10601069 forKey: ObjectIdentifier(process)
1061- ) {
1070+ ), tokenFileURL.path.hasPrefix(NSTemporaryDirectory()) {
10621071 Self.removeConnectionTokenFile(at: tokenFileURL)
10631072 }
10641073 }
@@ -1077,21 +1086,65 @@ final class VSCodeServeWebController {
10771086 }
10781087 }
10791088
1089+ /// Stable Application Support directory for VS Code serve-web state.
1090+ /// Mirrors the "programa" subdirectory convention used elsewhere in the app.
1091+ private static var vscodeServerDataDir: URL? {
1092+ FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
1093+ .first?
1094+ .appendingPathComponent("programa", isDirectory: true)
1095+ .appendingPathComponent("vscode-server", isDirectory: true)
1096+ }
1097+
10801098 private static func randomConnectionToken() -> String {
10811099 UUID().uuidString.replacingOccurrences(of: "-", with: "")
10821100 }
10831101
1102+ /// Returns a URL for the connection token file. Prefers a persistent file
1103+ /// under Application Support so the token (and the browser's vscode-tkn
1104+ /// cookie) survives Programa restarts (issue #21). Falls back to an
1105+ /// ephemeral temp-dir file when Application Support is unavailable.
10841106 private static func makeConnectionTokenFile() -> URL? {
1107+ if let persistentURL = vscodeServerDataDir?
1108+ .appendingPathComponent("connection-token", isDirectory: false) {
1109+ if let url = makePersistentConnectionTokenFile(at: persistentURL) {
1110+ return url
1111+ }
1112+ }
1113+ return makeEphemeralConnectionTokenFile()
1114+ }
1115+
1116+ private static func makePersistentConnectionTokenFile(at tokenFileURL: URL) -> URL? {
1117+ let dir = tokenFileURL.deletingLastPathComponent()
1118+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
1119+ // Reuse an existing non-empty token so the browser cookie stays valid.
1120+ if let existingData = try? Data(contentsOf: tokenFileURL), !existingData.isEmpty {
1121+ return tokenFileURL
1122+ }
1123+ let token = randomConnectionToken()
1124+ guard let tokenData = token.data(using: .utf8) else { return nil }
1125+ let fd = open(tokenFileURL.path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)
1126+ guard fd >= 0 else { return nil }
1127+ defer { _ = close(fd) }
1128+ let wroteAllBytes = tokenData.withUnsafeBytes { rawBuffer in
1129+ guard let baseAddress = rawBuffer.baseAddress else { return false }
1130+ return write(fd, baseAddress, rawBuffer.count) == rawBuffer.count
1131+ }
1132+ guard wroteAllBytes else {
1133+ try? FileManager.default.removeItem(at: tokenFileURL)
1134+ return nil
1135+ }
1136+ return tokenFileURL
1137+ }
1138+
1139+ private static func makeEphemeralConnectionTokenFile() -> URL? {
10851140 let token = randomConnectionToken()
10861141 let tokenFileName = "cmux-vscode-token-\(UUID().uuidString)"
10871142 let tokenFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
10881143 .appendingPathComponent(tokenFileName, isDirectory: false)
10891144 guard let tokenData = token.data(using: .utf8) else { return nil }
1090-
10911145 let fileDescriptor = open(tokenFileURL.path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)
10921146 guard fileDescriptor >= 0 else { return nil }
10931147 defer { _ = close(fileDescriptor) }
1094-
10951148 let wroteAllBytes = tokenData.withUnsafeBytes { rawBuffer in
10961149 guard let baseAddress = rawBuffer.baseAddress else { return false }
10971150 return write(fileDescriptor, baseAddress, rawBuffer.count) == rawBuffer.count
@@ -1100,7 +1153,6 @@ final class VSCodeServeWebController {
11001153 removeConnectionTokenFile(at: tokenFileURL)
11011154 return nil
11021155 }
1103-
11041156 return tokenFileURL
11051157 }
11061158
0 commit comments