Skip to content

Commit 239add9

Browse files
committed
fix(ctun): box exit code for Swift strict concurrency
Swift 5.10 / Swift 6 strict concurrency rejects mutating a captured `var` from inside `Task.detached`. Wrap the exit code in a tiny `ExitCodeBox` reference type (@unchecked Sendable) — the surrounding semaphore signal/wait still gives the happens-before relation needed for safe publication. CI runner with Xcode 15.4 hit this; local Xcode 15 was permissive.
1 parent 3799e9a commit 239add9

1 file changed

Lines changed: 17 additions & 7 deletions

File tree

Sources/ctun/Commands/StartCommand.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,14 @@ struct StartCommand: ParsableCommand {
9090

9191
private func waitForRunner(_ runner: CLIRunner) -> Int32 {
9292
let sem = DispatchSemaphore(value: 0)
93-
var exitCode: Int32 = 0
93+
let box = ExitCodeBox()
9494
Task.detached {
95-
exitCode = await runner.runForeground()
95+
let code = await runner.runForeground()
96+
box.value = code
9697
sem.signal()
9798
}
9899
sem.wait()
99-
return exitCode
100+
return box.value
100101
}
101102

102103
private func safeName(_ s: String) -> String {
@@ -127,15 +128,24 @@ struct RunDetachedCommand: ParsableCommand {
127128
)
128129
let runner = CLIRunner(tunnel: tunnel)
129130
let sem = DispatchSemaphore(value: 0)
130-
var exitCode: Int32 = 0
131+
let box = ExitCodeBox()
131132
Task.detached {
132-
exitCode = await runner.runForeground()
133+
let code = await runner.runForeground()
134+
box.value = code
133135
sem.signal()
134136
}
135137
sem.wait()
136138
PidFile.remove(for: tunnel.name)
137-
if exitCode != 0 {
138-
throw ExitCode(exitCode)
139+
if box.value != 0 {
140+
throw ExitCode(box.value)
139141
}
140142
}
141143
}
144+
145+
/// Boxed exit code shared between the synchronous CLI entry point and the
146+
/// detached Task that drives `CLIRunner`. Mutation under Swift's strict
147+
/// concurrency requires a reference type; correctness is guaranteed by the
148+
/// surrounding semaphore signal/wait happens-before relation.
149+
private final class ExitCodeBox: @unchecked Sendable {
150+
var value: Int32 = 0
151+
}

0 commit comments

Comments
 (0)