Skip to content

Commit 924e171

Browse files
authored
Refactor utilities under Foundation.Process and CartonHelpers.Process and share implementations (#477)
* organizer process utilities * resurrect ProcessResult.checkNonZeroExit * import Foundation * use SIGINT * remove didExit and tweak bundle completion message
1 parent 5eebe1f commit 924e171

File tree

11 files changed

+139
-114
lines changed

11 files changed

+139
-114
lines changed

Plugins/CartonBundlePlugin/CartonBundlePluginCommand.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,6 @@ struct CartonBundlePluginCommand: CommandPlugin {
8585
["--resources", $0.string]
8686
} + extractor.remainingArguments
8787
let frontend = try makeCartonFrontendProcess(context: context, arguments: frontendArguments)
88-
frontend.forwardTerminationSignals()
89-
try frontend.run()
90-
frontend.waitUntilExit()
91-
if frontend.terminationStatus == 0 {
92-
print("Bundle written in \(bundleDirectory)")
93-
}
94-
frontend.checkNonZeroExit()
88+
try frontend.checkRun(printsLoadingMessage: false, forwardExit: true)
9589
}
9690
}

Plugins/CartonDevPlugin/CartonDevPluginCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ struct CartonDevPluginCommand: CommandPlugin {
114114
}
115115

116116
frontend.waitUntilExit()
117-
frontend.checkNonZeroExit()
117+
frontend.forwardExit()
118118
}
119119

120120
private func defaultProduct(context: PluginContext) throws -> String {

Plugins/CartonPluginShared/PluginShared.swift

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,7 @@ internal func checkHelpFlag(_ arguments: [String], subcommand: String, context:
184184
if arguments.contains("--help") || arguments.contains("-h") {
185185
let frontend = try makeCartonFrontendProcess(
186186
context: context, arguments: [subcommand, "--help"])
187-
frontend.forwardTerminationSignals()
188-
try frontend.run()
189-
frontend.waitUntilExit()
190-
exit(frontend.terminationStatus)
187+
try frontend.checkRun(printsLoadingMessage: false, forwardExit: true)
191188
}
192189
}
193190

@@ -203,36 +200,3 @@ internal func makeCartonFrontendProcess(context: PluginContext, arguments: [Stri
203200
process.arguments = arguments
204201
return process
205202
}
206-
207-
internal func runCartonFrontend(context: PluginContext, arguments: [String]) throws -> Process {
208-
let process = try makeCartonFrontendProcess(context: context, arguments: arguments)
209-
try process.run()
210-
return process
211-
}
212-
213-
extension Process {
214-
internal func forwardTerminationSignals() {
215-
// Monitor termination/interrruption signals to forward them to child process
216-
func setSignalForwarding(_ signalNo: Int32) {
217-
signal(signalNo, SIG_IGN)
218-
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
219-
signalSource.setEventHandler {
220-
signalSource.cancel()
221-
self.interrupt()
222-
}
223-
signalSource.resume()
224-
}
225-
setSignalForwarding(SIGINT)
226-
setSignalForwarding(SIGTERM)
227-
228-
self.terminationHandler = {
229-
// Exit plugin process itself when child process exited
230-
exit($0.terminationStatus)
231-
}
232-
}
233-
internal func checkNonZeroExit() {
234-
if terminationStatus != 0 {
235-
exit(terminationStatus)
236-
}
237-
}
238-
}

Plugins/CartonTestPlugin/CartonTestPluginCommand.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,7 @@ struct CartonTestPluginCommand: CommandPlugin {
109109
["--resources", $0.string]
110110
} + extractor.remainingArguments
111111
let frontend = try makeCartonFrontendProcess(context: context, arguments: frontendArguments)
112-
frontend.forwardTerminationSignals()
113-
try frontend.run()
114-
frontend.waitUntilExit()
115-
frontend.checkNonZeroExit()
112+
try frontend.checkRun(printsLoadingMessage: false, forwardExit: true)
116113
}
117114

118115
private func buildDirectory(context: PluginContext) throws -> Path {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
struct CartonCoreError: Error & CustomStringConvertible {
2+
init(_ description: String) {
3+
self.description = description
4+
}
5+
var description: String
6+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Foundation
2+
3+
extension Foundation.Process {
4+
// Monitor termination/interrruption signals to forward them to child process
5+
public func setSignalForwarding(_ signalNo: Int32) {
6+
signal(signalNo, SIG_IGN)
7+
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
8+
signalSource.setEventHandler { [self] in
9+
signalSource.cancel()
10+
kill(processIdentifier, signalNo)
11+
}
12+
signalSource.resume()
13+
}
14+
15+
public func forwardTerminationSignals() {
16+
setSignalForwarding(SIGINT)
17+
setSignalForwarding(SIGTERM)
18+
}
19+
20+
public var commandLine: String {
21+
get throws {
22+
guard let executableURL else {
23+
throw CartonCoreError("executableURL is none")
24+
}
25+
26+
let commandLineArgs: [String] = [
27+
executableURL.path
28+
] + (arguments ?? [])
29+
30+
let q = "\""
31+
let commandLine: String = commandLineArgs
32+
.map { "\(q)\($0)\(q)" }
33+
.joined(separator: " ")
34+
35+
return commandLine
36+
}
37+
}
38+
39+
public func checkRun(
40+
printsLoadingMessage: Bool,
41+
forwardExit: Bool = false
42+
) throws {
43+
if printsLoadingMessage {
44+
print("Running \(try commandLine)")
45+
}
46+
47+
try run()
48+
forwardTerminationSignals()
49+
waitUntilExit()
50+
51+
if forwardExit {
52+
self.forwardExit()
53+
}
54+
55+
try checkNonZeroExit()
56+
}
57+
58+
public func forwardExit() {
59+
exit(terminationStatus)
60+
}
61+
62+
public func checkNonZeroExit() throws {
63+
if terminationStatus != 0 {
64+
throw CartonCoreError(
65+
"Process failed with status \(terminationStatus).\n" +
66+
"Command line: \(try commandLine)"
67+
)
68+
}
69+
}
70+
71+
public static func checkRun(
72+
_ executableURL: URL,
73+
arguments: [String],
74+
printsLoadingMessage: Bool = true,
75+
forwardExit: Bool = false
76+
) throws {
77+
let process = Foundation.Process()
78+
process.executableURL = executableURL
79+
process.arguments = arguments
80+
try process.checkRun(
81+
printsLoadingMessage: printsLoadingMessage,
82+
forwardExit: forwardExit
83+
)
84+
}
85+
}

Sources/CartonDriver/CartonDriverCommand.swift

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
//
3131
// This executable should be eventually removed once SwiftPM provides a way to express those requirements.
3232

33+
import CartonCore
3334
import CartonHelpers
3435
import Foundation
3536
import SwiftToolchain
@@ -41,49 +42,6 @@ struct CartonDriverError: Error & CustomStringConvertible {
4142
var description: String
4243
}
4344

44-
extension Foundation.Process {
45-
internal static func checkRun(
46-
_ executableURL: URL, arguments: [String], forwardExit: Bool = false
47-
) throws {
48-
let commandLine: String = ([executableURL.path] + arguments)
49-
.map { "\"\($0)\"" }.joined(separator: " ")
50-
51-
fputs("Running \(commandLine)\n", stderr)
52-
fflush(stderr)
53-
54-
let process = Foundation.Process()
55-
process.executableURL = executableURL
56-
process.arguments = arguments
57-
58-
// Monitor termination/interrruption signals to forward them to child process
59-
func setSignalForwarding(_ signalNo: Int32) {
60-
signal(signalNo, SIG_IGN)
61-
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
62-
signalSource.setEventHandler {
63-
signalSource.cancel()
64-
process.interrupt()
65-
}
66-
signalSource.resume()
67-
}
68-
setSignalForwarding(SIGINT)
69-
setSignalForwarding(SIGTERM)
70-
71-
try process.run()
72-
process.waitUntilExit()
73-
74-
if forwardExit {
75-
exit(process.terminationStatus)
76-
}
77-
78-
if process.terminationStatus != 0 {
79-
throw CartonDriverError(
80-
"Process failed with status \(process.terminationStatus).\n" +
81-
"Command line: \(commandLine)"
82-
)
83-
}
84-
}
85-
}
86-
8745
func derivePackageCommandArguments(
8846
swiftExec: URL,
8947
subcommand: String,
@@ -184,7 +142,10 @@ func pluginSubcommand(subcommand: String, argv0: String, arguments: [String]) as
184142
extraArguments: extraArguments
185143
)
186144

187-
try Foundation.Process.checkRun(swiftExec, arguments: pluginArguments, forwardExit: true)
145+
try Foundation.Process.checkRun(
146+
swiftExec, arguments: pluginArguments,
147+
forwardExit: true
148+
)
188149
}
189150

190151
public func main(arguments: [String]) async throws {
@@ -213,7 +174,8 @@ public func main(arguments: [String]) async throws {
213174
let (swiftPath, _) = try await toolchainSystem.inferSwiftPath(terminal)
214175
try Foundation.Process.checkRun(
215176
URL(fileURLWithPath: swiftPath.pathString),
216-
arguments: ["package"] + arguments.dropFirst(), forwardExit: true
177+
arguments: ["package"] + arguments.dropFirst(),
178+
forwardExit: true
217179
)
218180
case "--version":
219181
print(cartonVersion)

Sources/CartonFrontend/Commands/CartonFrontendBundleCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ struct CartonFrontendBundleCommand: AsyncParsableCommand {
135135
topLevelResourcePaths: resources
136136
)
137137

138-
terminal.write("Bundle generation finished successfully\n", inColor: .green, bold: true)
138+
terminal.write("Bundle successfully generated at \(bundleDirectory)\n", inColor: .green, bold: true)
139139
}
140140

141141
func optimize(_ inputPath: AbsolutePath, outputPath: AbsolutePath, terminal: InteractiveWriter)

Sources/CartonHelpers/ProcessEx.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import Foundation
2+
import Dispatch
3+
14
extension ProcessResult {
25
public mutating func setOutput(_ value: Result<[UInt8], any Swift.Error>) {
36
self = ProcessResult(
@@ -17,3 +20,28 @@ extension ProcessResult {
1720
return try utf8Output()
1821
}
1922
}
23+
24+
@discardableResult
25+
private func osSignal(
26+
_ sig: Int32,
27+
_ fn: (@convention(c) (Int32) -> Void)?
28+
) -> (@convention(c) (Int32) -> Void)? {
29+
signal(sig, fn)
30+
}
31+
32+
extension Process {
33+
public func setSignalForwarding(_ signalNo: Int32) {
34+
osSignal(signalNo, SIG_IGN)
35+
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
36+
signalSource.setEventHandler {
37+
signalSource.cancel()
38+
self.signal(signalNo)
39+
}
40+
signalSource.resume()
41+
}
42+
43+
public func forwardTerminationSignals() {
44+
setSignalForwarding(SIGINT)
45+
setSignalForwarding(SIGTERM)
46+
}
47+
}

Tests/CartonCommandTests/CommandTestHelper.swift

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,7 @@ func swiftRunProcess(
100100

101101
try process.launch()
102102

103-
func setSignalForwarding(_ signalNo: Int32) {
104-
signal(signalNo, SIG_IGN)
105-
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
106-
signalSource.setEventHandler {
107-
signalSource.cancel()
108-
process.signal(SIGINT)
109-
}
110-
signalSource.resume()
111-
}
112-
setSignalForwarding(SIGINT)
113-
setSignalForwarding(SIGTERM)
103+
process.forwardTerminationSignals()
114104

115105
return SwiftRunProcess(
116106
process: process,
@@ -137,7 +127,7 @@ func fetchWebContent(at url: URL, timeout: Duration) async throws -> (response:
137127
)
138128

139129
let (body, response) = try await session.data(for: request)
140-
130+
141131
guard let response = response as? HTTPURLResponse else {
142132
throw CommandTestError("Response from \(url.absoluteString) is not HTTPURLResponse")
143133
}

Tests/CartonCommandTests/DevCommandTests.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,23 @@ final class DevCommandTests: XCTestCase {
4343
// FIXME: Don't assume a specific port is available since it can be used by others or tests
4444
try await withFixture("EchoExecutable") { packageDirectory in
4545
let process = try swiftRunProcess(
46-
["carton", "dev", "--verbose", "--port", "8081", "--skip-auto-open"],
46+
["carton", "dev", "--verbose", "--port", "8080", "--skip-auto-open"],
4747
packageDirectory: packageDirectory.asURL
4848
)
4949

50-
try await checkForExpectedContent(process: process, at: "http://127.0.0.1:8081")
50+
try await checkForExpectedContent(process: process, at: "http://127.0.0.1:8080")
5151
}
5252
}
5353
#endif
5454

5555
private func fetchDevServerWithRetry(at url: URL) async throws -> (response: HTTPURLResponse, body: Data) {
5656
// client time out for connecting and responding
57-
let timeOut: Duration = .seconds(60)
57+
let timeOut: Duration = .seconds(10)
5858

5959
// client delay... let the server start up
60-
let delay: Duration = .seconds(30)
60+
let delay: Duration = .seconds(3)
6161

62-
// only try 5 times.
63-
let count = 5
62+
let count = 100
6463

6564
do {
6665
return try await withRetry(maxAttempts: count, initialDelay: delay, retryInterval: delay) {
@@ -78,7 +77,7 @@ final class DevCommandTests: XCTestCase {
7877
func checkForExpectedContent(process: SwiftRunProcess, at url: String) async throws {
7978
defer {
8079
// end the process regardless of success
81-
process.process.signal(SIGTERM)
80+
process.process.signal(SIGINT)
8281
}
8382

8483
let (response, data) = try await fetchDevServerWithRetry(at: try URL(string: url).unwrap("url"))

0 commit comments

Comments
 (0)