Skip to content

Commit d53983a

Browse files
authored
[5.6] Fix SIGINT handling during swift run (Cherry-pick of #4224, #4235, #4250) (#4231)
* Fix SIGINT handling during `swift run` (#4224) * Fix SIGINT handling during `swift run` * Create a safe wrapper of TSCBasic.exec * add test fixture for handling SIGINT on "swift run" (#4250) Co-authored-by: tomer doron <tomer@apple.com> rdar://90574791
1 parent 64adcf9 commit d53983a

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "TestableExe",
6+
targets: [
7+
.executableTarget(
8+
name: "Test",
9+
path: "."
10+
),
11+
]
12+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("done")

Sources/Commands/SwiftTool.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@ extension SwiftCommand {
255255
public static var _errorLabel: String { "error" }
256256
}
257257

258+
/// A safe wrapper of TSCBasic.exec.
259+
func exec(path: String, args: [String]) throws -> Never {
260+
#if !os(Windows)
261+
// On platforms other than Windows, signal(SIGINT, SIG_IGN) is used for handling SIGINT by DispatchSourceSignal,
262+
// but this process is about to be replaced by exec, so SIG_IGN must be returned to default.
263+
signal(SIGINT, SIG_DFL)
264+
#endif
265+
266+
try TSCBasic.exec(path: path, args: args)
267+
}
268+
258269
public class SwiftTool {
259270
#if os(Windows)
260271
// unfortunately this is needed for C callback handlers used by Windows shutdown handler

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
import Basics
1112
import XCTest
1213
import SPMTestSupport
1314
import TSCBasic
@@ -398,6 +399,86 @@ class MiscellaneousTestCase: XCTestCase {
398399
#endif
399400
}
400401

402+
func testSwiftRunSIGNINT() throws {
403+
try fixture(name: "Miscellaneous/SwiftRun") { fixturePath in
404+
let mainFilePath = fixturePath.appending(component: "main.swift")
405+
try localFileSystem.removeFileTree(mainFilePath)
406+
try localFileSystem.writeFileContents(mainFilePath) {
407+
"""
408+
import Foundation
409+
410+
print("sleeping")
411+
fflush(stdout)
412+
413+
sleep(10)
414+
print("done")
415+
"""
416+
}
417+
418+
let sync = DispatchGroup()
419+
let outputHandler = OutputHandler(sync: sync)
420+
let process = Process(
421+
arguments: [SwiftPMProduct.SwiftRun.path.pathString, "--package-path", fixturePath.pathString],
422+
outputRedirection: .stream(stdout: outputHandler.handle(bytes:), stderr: outputHandler.handle(bytes:))
423+
)
424+
425+
sync.enter()
426+
try process.launch()
427+
428+
// wait for the process to start
429+
if case .timedOut = sync.wait(timeout: .now() + 60) {
430+
return XCTFail("timeout waiting for process to start")
431+
}
432+
433+
// interrupt the process
434+
print("interrupting")
435+
process.signal(SIGINT)
436+
437+
// check for interrupt result
438+
let result = try process.waitUntilExit()
439+
XCTAssertEqual(result.exitStatus, .signalled(signal: 2))
440+
}
441+
442+
class OutputHandler {
443+
let sync: DispatchGroup
444+
var state = State.idle
445+
let lock = Lock()
446+
447+
init(sync: DispatchGroup) {
448+
self.sync = sync
449+
}
450+
451+
func handle(bytes: [UInt8]) {
452+
guard let output = String(bytes: bytes, encoding: .utf8) else {
453+
return
454+
}
455+
self.lock.withLock {
456+
switch self.state {
457+
case .idle:
458+
self.state = .buffering(output)
459+
case .buffering(let buffer):
460+
print(output, terminator: "")
461+
let newBuffer = buffer + output
462+
if newBuffer.contains("sleeping") {
463+
self.state = .done
464+
self.sync.leave()
465+
} else {
466+
self.state = .buffering(newBuffer)
467+
}
468+
case .done:
469+
break //noop
470+
}
471+
}
472+
}
473+
474+
enum State {
475+
case idle
476+
case buffering(String)
477+
case done
478+
}
479+
}
480+
}
481+
401482
func testReportingErrorFromGitCommand() throws {
402483
fixture(name: "Miscellaneous/MissingDependency") { prefix in
403484
// This fixture has a setup that is intentionally missing a local

0 commit comments

Comments
 (0)