-
Notifications
You must be signed in to change notification settings - Fork 406
Provide an interface for tools to implement SIGINFO handling.
#963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
29826f6
6ccd2b1
436fba6
6f067dc
64b3075
22f309a
d28b8ba
79f568d
26ddcf1
e1ab215
a6deea6
c06042e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift Argument Parser open source project | ||
| // | ||
| // Copyright (c) 2024 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import ArgumentParser | ||
| import Foundation | ||
|
|
||
| @MainActor | ||
| final class Info { | ||
| let start = Date() | ||
| var signalCount = 0 | ||
| } | ||
|
|
||
| let info = Info() | ||
|
|
||
| @main | ||
| @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) | ||
| struct InfoProvider: AsyncParsableCommand, InfoProvidingParsableCommand { | ||
| func run() async throws { | ||
| _ = info | ||
|
|
||
| #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) || os(FreeBSD) || os(OpenBSD) | ||
| print("Press Ctrl+T to see process information.") | ||
| #elseif os(Linux) || os(Android) | ||
| print("Send SIGUSR1 to \(getpid()) to see process information.") | ||
| #elseif os(Windows) | ||
| print("Press Ctrl+Break to see process information.") | ||
| #endif | ||
|
|
||
| let dot = [UInt8(ascii: ".")] | ||
| while true { | ||
| try await Task.sleep(nanoseconds: 1_000_000_000) | ||
| if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { | ||
| try? FileHandle.standardOutput.write(contentsOf: dot) | ||
| } else { | ||
| FileHandle.standardOutput.write(Data(dot)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @MainActor | ||
| static func provideInfo() { | ||
| let timeRunning = Date().timeIntervalSince(info.start) | ||
| print("Running for \(timeRunning) seconds.") | ||
|
|
||
| info.signalCount += 1 | ||
| #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) || os(FreeBSD) || os(OpenBSD) | ||
| print("SIGINFO received \(info.signalCount) time(s)!") | ||
| #elseif os(Linux) || os(Android) | ||
| print("SIGUSR1 received \(info.signalCount) time(s)!") | ||
| #elseif os(Windows) | ||
| print("Ctrl+Break received \(info.signalCount) time(s)!") | ||
| #endif | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift Argument Parser open source project | ||
| // | ||
| // Copyright (c) 2026 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| /// A parsable command that can provide information back to the user while it | ||
| /// runs. | ||
| /// | ||
| /// The user may request information from a running process via a | ||
| /// platform-specific mechanism: | ||
| /// | ||
| /// - On Apple platforms, FreeBSD, and OpenBSD, by sending `SIGINFO` to the | ||
| /// process or by pressing Ctrl+T in the Terminal application. | ||
| /// - On Linux, by sending the `SIGUSR1` signal to the process. | ||
| /// - On Windows, by pressing Ctrl+Break in the Terminal application. | ||
| /// | ||
| /// If your root command conforms to this protocol, Swift Argument Parser | ||
| /// automatically sets up a signal handler to listen for `SIGINFO` (or the | ||
| /// platform-specific equivalent). | ||
| /// | ||
| /// On platforms that do not support providing information, conformance to this | ||
| /// protocol has no effect. | ||
| @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) | ||
| public protocol InfoProvidingParsableCommand: Sendable, ParsableCommand { | ||
| #if compiler(>=6.2) | ||
| /// Provide information about the state of the process and about the | ||
| /// currently-running command. | ||
| /// | ||
| /// The information you provide is program-specific. Programs will often | ||
| /// provide a status update such as the percentage or count of work completed, | ||
| /// information about the currently-running work item, etc. | ||
| /// | ||
| /// - Important: Swift Argument Parser calls this function asynchronously | ||
| /// while your program is running. If your command type is actor-isolated, | ||
| /// ensure your ``AsyncParsableCommand/run()`` implementation periodically | ||
| /// yields control by suspending, sleeping, or calling [`Task.yield()`](https://developer.apple.com/documentation/swift/task/yield()). | ||
| /// | ||
| /// If ``AsyncParsableCommand/run()`` never yields, the Swift runtime may | ||
| /// not be able to schedule calls to this function and it will appear to the | ||
| /// user as if it is not implemented. | ||
| nonisolated(nonsending) static func provideInfo() async | ||
| #else | ||
| static func provideInfo() async | ||
| #endif | ||
| } | ||
|
|
||
| // MARK: - | ||
|
|
||
| @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) | ||
| extension SIGINFOHandler { | ||
| convenience init<T>(for command: T) where T: InfoProvidingParsableCommand { | ||
| self.init { | ||
| #if compiler(>=6.3) | ||
| if #available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *) { | ||
| _ = Task.immediate { | ||
| await T.provideInfo() | ||
| } | ||
| return | ||
| } | ||
| #endif | ||
|
|
||
| _ = Task { | ||
| await T.provideInfo() | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift Argument Parser open source project | ||
| // | ||
| // Copyright (c) 2026 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #if canImport(Dispatch) | ||
| private import Dispatch | ||
| #endif | ||
|
|
||
| #if canImport(Glibc) | ||
| import Glibc | ||
| #elseif canImport(Musl) | ||
| import Musl | ||
| #elseif canImport(Darwin) | ||
| import Darwin | ||
| #elseif canImport(WinSDK) | ||
| import WinSDK | ||
| #elseif canImport(WASILibc) | ||
| import WASILibc | ||
| #elseif canImport(Android) | ||
| import Android | ||
| #endif | ||
|
|
||
| /// A class whose instances represent `SIGINFO` handlers configured in this | ||
| /// process. | ||
| /// | ||
| /// - Note: This class is responsible for handling `SIGINFO`, `SIGUSR1` (Linux), | ||
| /// and Ctrl+Break (Windows). Naming is hard. | ||
| final class SIGINFOHandler: Sendable { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do wish that we had a better name than
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not API so we can change it. We're thinking about using it for Ctrl+C too to support cancellation. But for now, it's an implementation detail so I'm not sweating it that much.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I really like this if you are okay with the length. |
||
| /// The set of `SIGINFO` handlers configured in this process. | ||
| /// | ||
| /// Generally, this array will contain no more than `1` element, but it can be | ||
| /// larger if multiple commands are made to run in a single process. | ||
| private static let allSIGINFOHandlers = Mutex<[SIGINFOHandler]>([]) | ||
|
|
||
| #if canImport(Dispatch) | ||
|
grynspan marked this conversation as resolved.
|
||
| /// The queue on which SIGINFO handler callbacks are invoked. | ||
| /// | ||
| /// Perhaps the natural queue for signal handling is the main queue, but we do | ||
| /// not control user code and we cannot ensure that it doesn't block the main | ||
| /// queue/thread/actor for long periods of time, which would prevent our | ||
| /// dispatch source from ever firing its event handler. | ||
| private static let siginfoQueue = DispatchQueue( | ||
| label: "SIGINFO monitoring queue", | ||
| qos: .userInitiated, | ||
| autoreleaseFrequency: .workItem) | ||
|
|
||
| /// The dispatch source that listens for `SIGINFO` (or the platform-specific | ||
| /// equivalent). | ||
| /// | ||
| /// This declaration is annotated `nonisolated(unsafe)` because dispatch sources | ||
| /// do not conform to `Sendable` on non-Darwin targets. | ||
| private nonisolated(unsafe) static let siginfoSource: Any = { | ||
| #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) || os(FreeBSD) || os(OpenBSD) | ||
| let source = DispatchSource.makeSignalSource( | ||
| signal: SIGINFO, queue: siginfoQueue) | ||
| #elseif os(Linux) || os(Android) | ||
| // On Linux, SIGINFO is not defined, so we'll use SIGUSR1 for this purpose. | ||
| let source = DispatchSource.makeSignalSource( | ||
| signal: SIGUSR1, queue: siginfoQueue) | ||
| #elseif os(Windows) | ||
| let source = DispatchSource.makeUserDataAddSource(queue: siginfoQueue) | ||
| SetConsoleCtrlHandler( | ||
| { ctrlType in | ||
| guard ctrlType == CTRL_BREAK_EVENT else { | ||
| // Let the system handle it normally. | ||
| return false | ||
| } | ||
| if let siginfoSource = siginfoSource as? any DispatchSourceUserDataAdd { | ||
| siginfoSource.add(data: 1) | ||
| } | ||
| return true | ||
| }, true) | ||
| #else | ||
| // This platform does not support SIGINFO or any equivalent. To keep this | ||
| // code relatively simple, we still create a no-op dispatch source. | ||
| let source = DispatchSource.makeUserDataAddSource(queue: siginfoQueue) | ||
| #endif | ||
|
|
||
| source.setEventHandler { | ||
| // Invoke all registered handler objects. | ||
| let handlers = allSIGINFOHandlers.withLock { $0 } | ||
| for handler in handlers { | ||
| handler.handler() | ||
| } | ||
| } | ||
| source.activate() | ||
| return source | ||
| }() | ||
| #endif | ||
|
|
||
| /// The handler function this instance represents. | ||
| private let handler: @Sendable () -> Void | ||
|
|
||
| init(handlingWith handler: @escaping @Sendable () -> Void) { | ||
| self.handler = handler | ||
| } | ||
|
|
||
| /// Register this handler and start using it to listen for signals. | ||
| func register() { | ||
| Self.allSIGINFOHandlers.withLock { all in | ||
| all.append(self) | ||
| } | ||
|
|
||
| #if canImport(Dispatch) | ||
| // Ensure we're listening for signals after adding this handler. | ||
| _ = Self.siginfoSource | ||
| #endif | ||
| } | ||
|
|
||
| /// Unregister this handler and stop using it to listen for signals. | ||
| func unregister() { | ||
| Self.allSIGINFOHandlers.withLock { all in | ||
| all.removeAll { $0 === self } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this pattern occurs at least twice, would it be justified to do:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. I'm not too too bothered by it but I can add a helper if other folks want it.