Skip to content

Commit 833f3c8

Browse files
committed
Fix RunningProcess potentially hanging
1 parent 959fd54 commit 833f3c8

File tree

3 files changed

+37
-33
lines changed

3 files changed

+37
-33
lines changed

Sources/SourceKitBazelBSP/SharedUtils/Shell/RunningProcess.swift

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,65 +36,64 @@ extension Process: CommandLineProcess {
3636
}
3737
}
3838

39-
public struct RunningProcess: Sendable {
39+
public final class RunningProcess: Sendable {
4040
let cmd: String
4141
let stdout: Pipe
4242
let stderr: Pipe
4343
let wrappedProcess: CommandLineProcess
4444

45+
private nonisolated(unsafe) var stdoutData: Data = Data()
46+
private nonisolated(unsafe) var stderrData: Data = Data()
47+
private let group = DispatchGroup()
48+
4549
public init(cmd: String, stdout: Pipe, stderr: Pipe, wrappedProcess: CommandLineProcess) {
4650
self.cmd = cmd
4751
self.stdout = stdout
4852
self.stderr = stderr
4953
self.wrappedProcess = wrappedProcess
5054
}
5155

52-
public func result<T: DataConvertible>() throws -> T {
53-
let (stdoutData, stderrString): (T, String) = self.outputs()
54-
55-
guard wrappedProcess.terminationStatus == 0 else {
56-
logger.debug("Command failed: \(cmd)")
57-
throw ShellCommandRunnerError.failed(cmd, stderrString)
58-
}
59-
60-
return stdoutData
61-
}
62-
63-
public func outputs<T: DataConvertible>() -> (T, String) {
64-
nonisolated(unsafe) var stdoutData: Data = Data()
65-
nonisolated(unsafe) var stderrData: Data = Data()
66-
let group = DispatchGroup()
67-
56+
public func attachPipes() {
6857
// We need to read the pipes continuously to avoid hitting buffer limits.
6958
// See https://github.com/spotify/sourcekit-bazel-bsp/pull/65
7059
group.enter()
71-
stdout.fileHandleForReading.readabilityHandler = { stdoutFileHandle in
60+
stdout.fileHandleForReading.readabilityHandler = { [weak self] stdoutFileHandle in
7261
let tmpstdoutData = stdoutFileHandle.availableData
7362
if tmpstdoutData.isEmpty { // EOF
74-
stdout.fileHandleForReading.readabilityHandler = nil
75-
group.leave()
63+
self?.stdout.fileHandleForReading.readabilityHandler = nil
64+
self?.group.leave()
7665
} else {
77-
stdoutData.append(tmpstdoutData)
66+
self?.stdoutData.append(tmpstdoutData)
7867
}
7968
}
80-
8169
group.enter()
82-
stderr.fileHandleForReading.readabilityHandler = { stderrFileHandle in
70+
stderr.fileHandleForReading.readabilityHandler = { [weak self] stderrFileHandle in
8371
let tmpstderrData = stderrFileHandle.availableData
8472
if tmpstderrData.isEmpty { // EOF
85-
stderr.fileHandleForReading.readabilityHandler = nil
86-
group.leave()
73+
self?.stderr.fileHandleForReading.readabilityHandler = nil
74+
self?.group.leave()
8775
} else {
88-
stderrData.append(tmpstderrData)
76+
self?.stderrData.append(tmpstderrData)
8977
}
9078
}
79+
}
80+
81+
public func result<T: DataConvertible>() throws -> T {
82+
let (stdoutData, stderrString): (T, String) = self.outputs()
83+
84+
guard wrappedProcess.terminationStatus == 0 else {
85+
logger.debug("Command failed: \(self.cmd)")
86+
throw ShellCommandRunnerError.failed(cmd, stderrString)
87+
}
9188

92-
wrappedProcess.waitUntilExit()
93-
group.wait()
89+
return stdoutData
90+
}
9491

92+
public func outputs<T: DataConvertible>() -> (T, String) {
93+
group.wait()
94+
wrappedProcess.waitUntilExit()
9595
let stdoutResult = T.convert(from: stdoutData)
9696
let stderrResult = String(data: stderrData, encoding: .utf8) ?? "(no stderr)"
97-
9897
return (stdoutResult, stderrResult)
9998
}
10099

Sources/SourceKitBazelBSP/SharedUtils/Shell/ShellCommandRunner.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,18 @@ struct ShellCommandRunner: CommandRunner {
4646
process.arguments = ["-c", cmd]
4747
process.standardInput = nil
4848

49-
logger.info("Running shell: \(cmd)")
50-
try process.run()
51-
5249
let runningProcess = RunningProcess(
5350
cmd: cmd,
5451
stdout: stdout,
5552
stderr: stderr,
5653
wrappedProcess: process
5754
)
5855

56+
runningProcess.attachPipes()
57+
58+
logger.info("Running shell: \(cmd)")
59+
try process.run()
60+
5961
return runningProcess
6062
}
6163
}

Tests/SourceKitBazelBSPTests/Fakes/CommandRunnerFake.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ final class CommandRunnerFake: CommandRunner, @unchecked Sendable {
5454
guard let response = responses[cacheKey] else {
5555
throw CommandRunnerFakeError.unregisteredCommand(cmd, cwd)
5656
}
57+
let procFake = CommandLineProcessFake()
58+
let runningProc = RunningProcess(cmd: cmd, stdout: stdout, stderr: stderr, wrappedProcess: procFake)
59+
runningProc.attachPipes()
5760
stdout.fileHandleForWriting.write(response)
5861
stdout.fileHandleForWriting.closeFile()
5962
stderr.fileHandleForWriting.closeFile()
60-
return RunningProcess(cmd: cmd, stdout: stdout, stderr: stderr, wrappedProcess: CommandLineProcessFake())
63+
return runningProc
6164
}
6265

6366
private func key(for command: String, cwd: String?) -> String { return command + "|" + (cwd ?? "nil") }

0 commit comments

Comments
 (0)