Skip to content

Commit 4a3e51d

Browse files
committed
[Parseable Output] Emit Finished and Signalled Messages for each individual batch job primaries
Emulating existing C++ driver behavior. - Break up one job into multiple FinishedMessage/SignalledMessage messages
1 parent 77f3bc6 commit 4a3e51d

File tree

3 files changed

+182
-102
lines changed

3 files changed

+182
-102
lines changed

Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift

Lines changed: 169 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -76,29 +76,104 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
7676
stdoutStream <<< arguments.map { $0.spm_shellEscaped() }.joined(separator: " ") <<< "\n"
7777
stdoutStream.flush()
7878
case .parsableOutput:
79-
let beganMessages = constructJobBeganMessages(job: job, arguments: arguments, pid: pid)
80-
for beganMessage in beganMessages {
81-
let message = ParsableMessage(name: job.kind.rawValue, kind: .began(beganMessage))
79+
let messages = constructJobBeganMessages(job: job, arguments: arguments, pid: pid)
80+
for beganMessage in messages {
81+
emit(ParsableMessage(name: job.kind.rawValue, kind: .began(beganMessage)))
82+
}
83+
}
84+
}
85+
86+
public func jobFinished(job: Job, result: ProcessResult, pid: Int) {
87+
if showJobLifecycle {
88+
diagnosticEngine.emit(.remark_job_lifecycle("Finished", job))
89+
}
90+
91+
buildRecordInfo?.jobFinished(job: job, result: result)
92+
93+
// FIXME: Currently, TSCBasic.Process uses NSProcess on Windows and discards
94+
// the bits of the exit code used to differentiate between normal and abnormal
95+
// termination.
96+
#if !os(Windows)
97+
if case .signalled = result.exitStatus {
98+
anyJobHadAbnormalExit = true
99+
}
100+
#endif
101+
102+
switch mode {
103+
case .regular, .verbose:
104+
let output = (try? result.utf8Output() + result.utf8stderrOutput()) ?? ""
105+
if !output.isEmpty {
106+
Driver.stdErrQueue.sync {
107+
stderrStream <<< output
108+
stderrStream.flush()
109+
}
110+
}
111+
112+
case .parsableOutput:
113+
let output = (try? result.utf8Output() + result.utf8stderrOutput()).flatMap { $0.isEmpty ? nil : $0 }
114+
let messages: [ParsableMessage]
115+
116+
switch result.exitStatus {
117+
case .terminated(let code):
118+
messages = constructJobFinishedMessages(job: job, exitCode: code, output: output,
119+
pid: pid).map {
120+
ParsableMessage(name: job.kind.rawValue, kind: .finished($0))
121+
}
122+
#if !os(Windows)
123+
case .signalled(let signal):
124+
let errorMessage = strsignal(signal).map { String(cString: $0) } ?? ""
125+
messages = constructJobSignalledMessages(job: job, error: errorMessage, output: output,
126+
signal: signal, pid: pid).map {
127+
ParsableMessage(name: job.kind.rawValue, kind: .signalled($0))
128+
}
129+
#endif
130+
}
131+
for message in messages {
82132
emit(message)
83133
}
84134
}
85135
}
86136

87-
public func constructJobBeganMessages(job: Job, arguments: [String], pid: Int) -> [BeganMessage] {
137+
public func jobSkipped(job: Job) {
138+
if showJobLifecycle {
139+
diagnosticEngine.emit(.remark_job_lifecycle("Skipped", job))
140+
}
141+
switch mode {
142+
case .regular, .verbose:
143+
break
144+
case .parsableOutput:
145+
let skippedMessage = SkippedMessage(inputs: job.displayInputs.map{ $0.file.name })
146+
let message = ParsableMessage(name: job.kind.rawValue, kind: .skipped(skippedMessage))
147+
emit(message)
148+
}
149+
}
150+
151+
private func emit(_ message: ParsableMessage) {
152+
// FIXME: Do we need to do error handling here? Can this even fail?
153+
guard let json = try? message.toJSON() else { return }
154+
Driver.stdErrQueue.sync {
155+
stderrStream <<< json.count <<< "\n"
156+
stderrStream <<< String(data: json, encoding: .utf8)! <<< "\n"
157+
stderrStream.flush()
158+
}
159+
}
160+
}
161+
162+
// MARK: - Message Construction
163+
/// Generation of messages from jobs, including breaking down batch compile jobs into constituent messages.
164+
private extension ToolExecutionDelegate {
165+
166+
// MARK: - Job Began
167+
func constructJobBeganMessages(job: Job, arguments: [String], pid: Int) -> [BeganMessage] {
88168
let result : [BeganMessage]
89-
if job.kind == .compile {
90-
if job.primaryInputs.count == 1 {
91-
result = [constructSingleBeganMessage(inputs: job.displayInputs,
92-
outputs: job.outputs,
93-
arguments: arguments,
94-
pid: pid,
95-
realPid: pid)]
96-
} else {
97-
// Batched compile jobs need to be broken up into multiple messages, one per constituent.
98-
result = constructBatchCompileBeginMessages(job: job, arguments: arguments, pid: pid,
99-
quasiPIDBase: nextBatchQuasiPID)
100-
nextBatchQuasiPID -= result.count
101-
}
169+
if job.kind == .compile,
170+
job.primaryInputs.count > 1 {
171+
// Batched compile jobs need to be broken up into multiple messages, one per constituent.
172+
result = constructBatchCompileBeginMessages(job: job, arguments: arguments, pid: pid,
173+
quasiPIDBase: nextBatchQuasiPID)
174+
// Today, parseable-output messages are constructed and emitted synchronously
175+
// on `MultiJobExecutor`'s `delegateQueue`. This is why the below operation is safe.
176+
nextBatchQuasiPID -= result.count
102177
} else {
103178
result = [constructSingleBeganMessage(inputs: job.displayInputs,
104179
outputs: job.outputs,
@@ -110,8 +185,8 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
110185
return result
111186
}
112187

113-
public func constructBatchCompileBeginMessages(job: Job, arguments: [String],
114-
pid: Int, quasiPIDBase: Int) -> [BeganMessage] {
188+
func constructBatchCompileBeginMessages(job: Job, arguments: [String], pid: Int,
189+
quasiPIDBase: Int) -> [BeganMessage] {
115190
precondition(job.kind == .compile && job.primaryInputs.count > 1)
116191
var quasiPID = quasiPIDBase
117192
var result : [BeganMessage] = []
@@ -137,12 +212,8 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
137212
return result
138213
}
139214

140-
public func constructSingleBeganMessage(inputs: [TypedVirtualPath],
141-
outputs: [TypedVirtualPath],
142-
arguments: [String],
143-
pid: Int,
144-
realPid: Int) -> BeganMessage {
145-
215+
func constructSingleBeganMessage(inputs: [TypedVirtualPath], outputs: [TypedVirtualPath],
216+
arguments: [String], pid: Int, realPid: Int) -> BeganMessage {
146217
let outputs: [BeganMessage.Output] = outputs.map {
147218
.init(path: $0.file.name, type: $0.type.description)
148219
}
@@ -157,10 +228,82 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
157228
)
158229
}
159230

231+
// MARK: - Job Finished
232+
func constructJobFinishedMessages(job: Job, exitCode: Int32, output: String?, pid: Int)
233+
-> [FinishedMessage] {
234+
let result : [FinishedMessage]
235+
if job.kind == .compile,
236+
job.primaryInputs.count > 1 {
237+
result = constructBatchCompileFinishedMessages(job: job, exitCode: exitCode,
238+
output: output, pid: pid)
239+
} else {
240+
result = [constructSingleFinishedMessage(exitCode: exitCode, output: output,
241+
pid: pid, realPid: pid)]
242+
}
243+
return result
244+
}
245+
246+
func constructBatchCompileFinishedMessages(job: Job, exitCode: Int32, output: String?, pid: Int)
247+
-> [FinishedMessage] {
248+
precondition(job.kind == .compile && job.primaryInputs.count > 1)
249+
var result : [FinishedMessage] = []
250+
for input in job.primaryInputs {
251+
guard let quasiPid = batchJobInputQuasiPIDMap[(job, input)] else {
252+
fatalError("Parsable-Output batch sub-job finished with no matching started message: \(job.description) : \(input.file.description)")
253+
}
254+
result.append(
255+
constructSingleFinishedMessage(exitCode: exitCode, output: output,
256+
pid: quasiPid, realPid: pid))
257+
}
258+
return result
259+
}
260+
261+
func constructSingleFinishedMessage(exitCode: Int32, output: String?, pid: Int, realPid: Int)
262+
-> FinishedMessage {
263+
return FinishedMessage(exitStatus: Int(exitCode), output: output, pid: pid, realPid: realPid)
264+
}
265+
266+
// MARK: - Job Signalled
267+
func constructJobSignalledMessages(job: Job, error: String, output: String?,
268+
signal: Int32, pid: Int) -> [SignalledMessage] {
269+
let result : [SignalledMessage]
270+
if job.kind == .compile,
271+
job.primaryInputs.count > 1 {
272+
result = constructBatchCompileSignalledMessages(job: job, error: error, output: output,
273+
signal: signal, pid: pid)
274+
} else {
275+
result = [constructSingleSignalledMessage(error: error, output: output, signal: signal,
276+
pid: pid, realPid: pid)]
277+
}
278+
return result
279+
}
280+
281+
func constructBatchCompileSignalledMessages(job: Job, error: String, output: String?,
282+
signal: Int32, pid: Int)
283+
-> [SignalledMessage] {
284+
precondition(job.kind == .compile && job.primaryInputs.count > 1)
285+
var result : [SignalledMessage] = []
286+
for input in job.primaryInputs {
287+
guard let quasiPid = batchJobInputQuasiPIDMap[(job, input)] else {
288+
fatalError("Parsable-Output batch sub-job signalled with no matching started message: \(job.description) : \(input.file.description)")
289+
}
290+
result.append(
291+
constructSingleSignalledMessage(error: error, output: output, signal: signal,
292+
pid: quasiPid, realPid: pid))
293+
}
294+
return result
295+
}
296+
297+
func constructSingleSignalledMessage(error: String, output: String?, signal: Int32,
298+
pid: Int, realPid: Int)
299+
-> SignalledMessage {
300+
return SignalledMessage(pid: pid, realPid: realPid, output: output,
301+
errorMessage: error, signal: Int(signal))
302+
}
160303

161304
/// Best-effort attempt to "fix-up" the individual swift-frontend invocation command line, to pretend
162305
/// it is an individual single-primary compile job, rather than a batch mode compile with multiple primaries
163-
private static func filterPrimaryArguments(in arguments: [String],
306+
static func filterPrimaryArguments(in arguments: [String],
164307
input: TypedVirtualPath,
165308
outputs: [TypedVirtualPath]) -> [String] {
166309
// We must have only one `-primary-file` option specified, the one that corresponds
@@ -195,76 +338,6 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
195338

196339
return result
197340
}
198-
199-
public func jobFinished(job: Job, result: ProcessResult, pid: Int) {
200-
if showJobLifecycle {
201-
diagnosticEngine.emit(.remark_job_lifecycle("Finished", job))
202-
}
203-
204-
buildRecordInfo?.jobFinished(job: job, result: result)
205-
206-
// FIXME: Currently, TSCBasic.Process uses NSProcess on Windows and discards
207-
// the bits of the exit code used to differentiate between normal and abnormal
208-
// termination.
209-
#if !os(Windows)
210-
if case .signalled = result.exitStatus {
211-
anyJobHadAbnormalExit = true
212-
}
213-
#endif
214-
215-
switch mode {
216-
case .regular, .verbose:
217-
let output = (try? result.utf8Output() + result.utf8stderrOutput()) ?? ""
218-
if !output.isEmpty {
219-
Driver.stdErrQueue.sync {
220-
stderrStream <<< output
221-
stderrStream.flush()
222-
}
223-
}
224-
225-
case .parsableOutput:
226-
let output = (try? result.utf8Output() + result.utf8stderrOutput()).flatMap { $0.isEmpty ? nil : $0 }
227-
let message: ParsableMessage
228-
229-
switch result.exitStatus {
230-
case .terminated(let code):
231-
let finishedMessage = FinishedMessage(exitStatus: Int(code), pid: pid, output: output)
232-
message = ParsableMessage(name: job.kind.rawValue, kind: .finished(finishedMessage))
233-
234-
#if !os(Windows)
235-
case .signalled(let signal):
236-
let errorMessage = strsignal(signal).map { String(cString: $0) } ?? ""
237-
let signalledMessage = SignalledMessage(pid: pid, output: output, errorMessage: errorMessage, signal: Int(signal))
238-
message = ParsableMessage(name: job.kind.rawValue, kind: .signalled(signalledMessage))
239-
#endif
240-
}
241-
emit(message)
242-
}
243-
}
244-
245-
public func jobSkipped(job: Job) {
246-
if showJobLifecycle {
247-
diagnosticEngine.emit(.remark_job_lifecycle("Skipped", job))
248-
}
249-
switch mode {
250-
case .regular, .verbose:
251-
break
252-
case .parsableOutput:
253-
let skippedMessage = SkippedMessage(inputs: job.displayInputs.map{ $0.file.name })
254-
let message = ParsableMessage(name: job.kind.rawValue, kind: .skipped(skippedMessage))
255-
emit(message)
256-
}
257-
}
258-
259-
private func emit(_ message: ParsableMessage) {
260-
// FIXME: Do we need to do error handling here? Can this even fail?
261-
guard let json = try? message.toJSON() else { return }
262-
Driver.stdErrQueue.sync {
263-
stderrStream <<< json.count <<< "\n"
264-
stderrStream <<< String(data: json, encoding: .utf8)! <<< "\n"
265-
stderrStream.flush()
266-
}
267-
}
268341
}
269342

270343
fileprivate extension Diagnostic.Message {

Sources/SwiftDriver/Execution/ParsableOutput.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,42 +110,47 @@ import Foundation
110110
@_spi(Testing) public struct FinishedMessage: Encodable {
111111
let exitStatus: Int
112112
let pid: Int
113+
let process: ActualProcess
113114
let output: String?
114115

115-
// proc-info
116-
117116
public init(
118117
exitStatus: Int,
118+
output: String?,
119119
pid: Int,
120-
output: String?
120+
realPid: Int
121121
) {
122122
self.exitStatus = exitStatus
123123
self.pid = pid
124+
self.process = ActualProcess(realPid: realPid)
124125
self.output = output
125126
}
126127

127128
private enum CodingKeys: String, CodingKey {
128129
case pid
130+
case process
129131
case output
130132
case exitStatus = "exit-status"
131133
}
132134
}
133135

134136
@_spi(Testing) public struct SignalledMessage: Encodable {
135137
let pid: Int
138+
let process: ActualProcess
136139
let output: String?
137140
let errorMessage: String
138141
let signal: Int
139142

140-
public init(pid: Int, output: String?, errorMessage: String, signal: Int) {
143+
public init(pid: Int, realPid: Int, output: String?, errorMessage: String, signal: Int) {
141144
self.pid = pid
145+
self.process = ActualProcess(realPid: realPid)
142146
self.output = output
143147
self.errorMessage = errorMessage
144148
self.signal = signal
145149
}
146150

147151
private enum CodingKeys: String, CodingKey {
148152
case pid
153+
case process
149154
case output
150155
case errorMessage = "error-message"
151156
case signal

Tests/SwiftDriverTests/ParsableMessageTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ final class ParsableMessageTests: XCTestCase {
2020
func testBeganMessage() throws {
2121
let message = BeganMessage(
2222
pid: 1,
23+
realPid: 1,
2324
inputs: ["/path/to/foo.swift"],
2425
outputs: [
2526
.init(path: "/path/to/foo.o", type: "object")
@@ -57,7 +58,7 @@ final class ParsableMessageTests: XCTestCase {
5758
}
5859

5960
func testFinishedMessage() throws {
60-
let message = FinishedMessage(exitStatus: 1, pid: 1, output: "hello")
61+
let message = FinishedMessage(exitStatus: 1, output: "hello", pid: 1, realPid: 1)
6162
let finishedMessage = ParsableMessage(name: "compile", kind: .finished(message))
6263
let encoded = try finishedMessage.toJSON()
6364
let string = String(data: encoded, encoding: .utf8)!
@@ -74,7 +75,8 @@ final class ParsableMessageTests: XCTestCase {
7475
}
7576

7677
func testSignalledMessage() throws {
77-
let message = SignalledMessage(pid: 2, output: "sig", errorMessage: "err", signal: 3)
78+
let message = SignalledMessage(pid: 2, realPid: 2, output: "sig",
79+
errorMessage: "err", signal: 3)
7880
let signalledMessage = ParsableMessage(name: "compile", kind: .signalled(message))
7981
let encoded = try signalledMessage.toJSON()
8082
let string = String(data: encoded, encoding: .utf8)!

0 commit comments

Comments
 (0)