├── .gitignore ├── .spi.yml ├── .swift-format ├── .vscode └── settings.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md └── Sources ├── CSystemd ├── module.modulemap └── systemd.h ├── Example ├── example-systemd.service └── main.swift ├── Systemd ├── Extensions │ ├── LoggerLevel+syslogPriority.swift │ └── String+trimmingCharacters.swift ├── SystemdHelpers.swift ├── SystemdJournal.swift ├── SystemdNotifier.swift └── Utility.swift └── SystemdLifecycle └── SystemdService.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Swift 2 | .build*/ 3 | .swiftpm/ 4 | 5 | # VSCode 6 | launch.json 7 | 8 | # Swiftly 9 | .swift-version 10 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Systemd, SystemdLifecycle] 5 | platform: linux -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 120, 4 | "indentation": { 5 | "spaces": 4 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[swift]": { 3 | "editor.formatOnSave": true 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jesse L. Zamora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-async-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-async-algorithms", 7 | "state" : { 8 | "revision" : "4c3ea81f81f0a25d0470188459c6d4bf20cf2f97", 9 | "version" : "1.0.3" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 18 | "version" : "1.1.4" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-docc-plugin", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-docc-plugin", 25 | "state" : { 26 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 27 | "version" : "1.4.3" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-docc-symbolkit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 34 | "state" : { 35 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 36 | "version" : "1.0.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-log", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-log.git", 43 | "state" : { 44 | "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", 45 | "version" : "1.6.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-service-lifecycle", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/swift-server/swift-service-lifecycle.git", 52 | "state" : { 53 | "revision" : "c2e97cf6f81510f2d6b4a69453861db65d478560", 54 | "version" : "2.6.3" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-systemd", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | ], 14 | products: [ 15 | .library(name: "Systemd", targets: ["Systemd"]), 16 | .library(name: "SystemdLifecycle", targets: ["SystemdLifecycle"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), 20 | .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), 21 | .package(url: "https://github.com/apple/swift-log", from: "1.6.2"), 22 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"), 23 | ], 24 | targets: [ 25 | .systemLibrary( 26 | name: "CSystemd", 27 | providers: [ 28 | .apt(["libsystemd-dev"]), 29 | .yum(["systemd-devel"]), 30 | ] 31 | ), 32 | .target( 33 | name: "Systemd", 34 | dependencies: [ 35 | .product(name: "Logging", package: "swift-log"), 36 | "CSystemd", 37 | ] 38 | ), 39 | .target( 40 | name: "SystemdLifecycle", 41 | dependencies: [ 42 | .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), 43 | .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), 44 | "Systemd", 45 | ] 46 | ), 47 | .executableTarget( 48 | name: "Example", 49 | dependencies: ["SystemdLifecycle"], 50 | exclude: ["example-systemd.service"] 51 | ), 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Systemd 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fxtremekforever%2Fswift-systemd%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/xtremekforever/swift-systemd) 4 | 5 | A simple Swift library to interface with systemd on Linux. 6 | 7 | ## Compatibility 8 | 9 | This library is designed to be API-compatible on non-Linux platforms (like macOS), so if an 10 | application uses this package it will still compile and run on non-Linux platforms. However, 11 | systemd-specific calls will have no effect. For example, `SystemdHelpers.isSystemdService` will 12 | always return `false` on Windows or macOS. 13 | 14 | To use this library in Linux, however, the `systemd` libraries are required. These can be installed 15 | with the following commands on different distributions: 16 | 17 | * Debian/Ubuntu: `sudo apt install libsystemd-dev` 18 | * RHEL/Fedora: `sudo dnf install systemd-devel` 19 | * SUSE/OpenSUSE: `sudo zypper install systemd-devel` 20 | 21 | For other distributions, look in the package repositories for a systemd dev package and install it. 22 | 23 | NOTE: This library is *NOT* compatible with Musl as it appears that systemd libraries are still 24 | [not fully ported to Musl yet](https://catfox.life/2024/09/05/porting-systemd-to-musl-libc-powered-linux/). 25 | Please open a ticket if this changes so that support can be added for Musl, once this configuration 26 | is supported. 27 | 28 | ## Installation 29 | 30 | Add the following dependency to your `Package.swift` file: 31 | 32 | ```swift 33 | .package(url: "https://github.com/xtremekforever/swift-systemd.git", from: "0.1.0") 34 | ``` 35 | 36 | Then, add it to your target `dependencies` section like this: 37 | 38 | ```swift 39 | .product(name: "Systemd", package: "swift-systemd") 40 | ``` 41 | 42 | ## Usage 43 | 44 | Take this example systemd service file: 45 | 46 | ```ini 47 | [Unit] 48 | Description=My Systemd Service 49 | 50 | [Service] 51 | Type=notify 52 | WorkingDirectory=/usr/local/bin 53 | ExecStart=/usr/local/bin/MyService 54 | WatchdogSec=30 55 | 56 | [Install] 57 | WantedBy=multi-user.target 58 | ``` 59 | 60 | First, add `import Systemd` to the app use the modules provided by this library. 61 | 62 | To see if the app is running under systemd, use the `SystemdHelpers` interface: 63 | 64 | ```swift 65 | if SystemdHelpers.isSystemdService { 66 | print("This app is running as a systemd service!") 67 | 68 | // do things like modify logging format (if using swift-log) or whatever else is needed. 69 | } 70 | ``` 71 | 72 | To send signals to systemd about the state of the app, use the `SystemdNotifier` interface: 73 | 74 | ```swift 75 | let notifier = SystemdNotifier() 76 | 77 | // Call after starting up app (sends READY=1) 78 | notifier.notify(ServiceState.Ready) 79 | 80 | // Call before exiting app (sends STOPPING=1) 81 | notifier.notify(ServiceState.Stopping) 82 | ``` 83 | 84 | ### Systemd Lifecycle 85 | 86 | This repo also contains a separate `SystemdLifecycle` product that can be used by projects that employ the [swift-service-lifecycle](https://github.com/swift-server/swift-service-lifecycle) library to run and manage application services. It is a simple service that sends the `READY=1` and `STOPPING=1` signals above from the service `run()` method. 87 | 88 | It can be used by adding the `SystemdLifecycle` target `dependencies` section like this: 89 | 90 | ```swift 91 | .product(name: "SystemdLifecycle", package: "swift-systemd") 92 | ``` 93 | 94 | Then, once the product is imported with `import SystemdLifecycle`, it can be added to a `ServiceGroup`: 95 | 96 | ```swift 97 | let serviceGroup = ServiceGroup( 98 | configuration: .init( 99 | services: [ 100 | .init(service: SystemdService()) 101 | ] 102 | ) 103 | ) 104 | try await serviceGroup.run() 105 | ``` 106 | 107 | `SystemdService` does not have any dependencies on other services, so it can be constructed and started at any point in the application's service lifecycle. 108 | -------------------------------------------------------------------------------- /Sources/CSystemd/module.modulemap: -------------------------------------------------------------------------------- 1 | module CSystemd [system] { 2 | header "systemd.h" 3 | link "systemd" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/CSystemd/systemd.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | -------------------------------------------------------------------------------- /Sources/Example/example-systemd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Example Swift Systemd Service 3 | 4 | [Service] 5 | Type=notify 6 | WorkingDirectory=/usr/local/bin 7 | ExecStart=/usr/local/bin/Example 8 | WatchdogSec=30 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /Sources/Example/main.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | import ServiceLifecycle 3 | import Systemd 4 | import SystemdLifecycle 5 | 6 | let label = "SystemdExample" 7 | let handler = MultiplexLogHandler([ 8 | StreamLogHandler.standardOutput(label: label), 9 | SystemdJournalLogHandler(label: label), 10 | ]) 11 | let logger = Logger(label: label, factory: { _ in handler }) 12 | 13 | if SystemdHelpers.isSystemdService { 14 | logger.info("Running as systemd service!") 15 | } else { 16 | logger.info("Not running as a systemd service...") 17 | } 18 | 19 | if #available(iOS 16.0, tvOS 16.0, watchOS 9.0, *) { 20 | logger.info("Adding SystemdService to run as part of a ServiceGroup...") 21 | let serviceGroup = ServiceGroup( 22 | configuration: .init( 23 | services: [ 24 | .init(service: SystemdService()) 25 | ], 26 | gracefulShutdownSignals: [.sigterm], 27 | logger: logger 28 | ) 29 | ) 30 | 31 | logger.info("Send SIGTERM signal to exit the service") 32 | try await serviceGroup.run() 33 | } 34 | 35 | logger.info("Exiting application...") 36 | -------------------------------------------------------------------------------- /Sources/Systemd/Extensions/LoggerLevel+syslogPriority.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | 3 | // Only available on Linux 4 | #if os(Linux) 5 | import CSystemd 6 | 7 | extension Logger.Level { 8 | var syslogPriority: CInt { 9 | switch self { 10 | case .trace, .debug: 11 | return LOG_DEBUG 12 | case .info: 13 | return LOG_INFO 14 | case .notice: 15 | return LOG_NOTICE 16 | case .warning: 17 | return LOG_WARNING 18 | case .error: 19 | return LOG_ERR 20 | case .critical: 21 | return LOG_CRIT 22 | } 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/Systemd/Extensions/String+trimmingCharacters.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | extension String { 14 | // NOTE: This is copied from the FoundationEssentials module while we still don't 15 | // have an official .trimmingCharacters method provided by FoundationEssentials. 16 | // https://github.com/swiftlang/swift-foundation/tree/main/Sources/FoundationEssentials/String/BidirectionalCollection.swift#L37 17 | func trimmingCharacters(while predicate: (Element) -> Bool) -> SubSequence { 18 | var idx = startIndex 19 | while idx < endIndex && predicate(self[idx]) { 20 | formIndex(after: &idx) 21 | } 22 | 23 | let startOfNonTrimmedRange = idx // Points at the first char not in the set 24 | guard startOfNonTrimmedRange != endIndex else { 25 | return self[endIndex...] 26 | } 27 | 28 | let beforeEnd = index(before: endIndex) 29 | guard startOfNonTrimmedRange < beforeEnd else { 30 | return self[startOfNonTrimmedRange.. 0 { 30 | return .microseconds(usec) 31 | } else if ret == 0 { 32 | return nil // Watchdog disabled 33 | } else { 34 | let error = String(cString: strerror(-ret)) 35 | print("Unable to get watchdog configuration: \(error)") 36 | return nil 37 | } 38 | #else 39 | return nil 40 | #endif 41 | } 42 | 43 | /// Whether or not the watchdog is enabled. 44 | /// 45 | /// This is a simple variable computed from the `watchdogTimeout` to determine if the 46 | /// watchdog is enabled for this service. 47 | /// 48 | /// - Returns: `true` if watchdog is enabled/configured, `false` otherwise. 49 | @available(iOS 16.0, tvOS 16.0, watchOS 9.0, *) 50 | public static var watchdogEnabled: Bool { watchdogTimeout != nil } 51 | 52 | /// Recommended interval to send watchdog notification. 53 | /// 54 | /// Defaults to half of the configured watchdog timeout. 55 | /// 56 | /// - Returns: `Duration` with the recommended interval, `nil` if not configured. 57 | @available(iOS 16.0, tvOS 16.0, watchOS 9.0, *) 58 | public static var watchdogRecommendedNotifyInterval: Duration? { watchdogTimeout.map { $0 / 2 } } 59 | 60 | private static func getIsSystemdService() -> Bool { 61 | #if os(Linux) 62 | let pid = Glibc.getppid() 63 | do { 64 | let name = try String(contentsOfFile: "/proc/\(pid)/comm", encoding: .utf8) 65 | .trimmingCharacters(while: \.isWhitespace) 66 | return name == "systemd" 67 | } catch { 68 | print("Unable to determine if running systemd: \(error)") 69 | } 70 | #endif 71 | 72 | return false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Systemd/SystemdJournal.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | 3 | #if os(Linux) 4 | import CSystemd 5 | #endif 6 | 7 | public struct SystemdJournalLogHandler: LogHandler { 8 | public var logLevel: Logger.Level = .info 9 | public var metadataProvider: Logger.MetadataProvider? 10 | 11 | private let label: String 12 | 13 | @Sendable 14 | public init( 15 | label: String, 16 | metadataProvider: Logger.MetadataProvider? = nil 17 | ) { 18 | self.label = label 19 | self.metadataProvider = metadataProvider 20 | } 21 | 22 | @Sendable 23 | public init( 24 | label: String 25 | ) { 26 | self.init(label: label, metadataProvider: nil) 27 | } 28 | 29 | public func log( 30 | level: Logger.Level, 31 | message: Logger.Message, 32 | metadata explicitMetadata: Logger.Metadata?, 33 | source: String, 34 | file: String, 35 | function: String, 36 | line: UInt 37 | ) { 38 | #if os(Linux) 39 | let effectiveMetadata = Self.prepareMetadata( 40 | label: label, 41 | level: level, 42 | message: message, 43 | base: metadata, 44 | provider: metadataProvider, 45 | explicit: explicitMetadata, 46 | source: source, 47 | file: file, 48 | function: function, 49 | line: line 50 | ) 51 | 52 | withArrayOfIovecs(effectiveMetadata.map { "\($0)=\($1)" }) { iov in 53 | _ = sd_journal_sendv(iov, Int32(iov.count)) 54 | } 55 | #endif 56 | } 57 | 58 | public var metadata = Logger.Metadata() 59 | 60 | public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { 61 | get { 62 | metadata[metadataKey] 63 | } 64 | set(newValue) { 65 | metadata[metadataKey] = newValue 66 | } 67 | } 68 | 69 | #if os(Linux) 70 | private static func prepareMetadata( 71 | label: String, 72 | level: Logger.Level, 73 | message: Logger.Message, 74 | base: Logger.Metadata, 75 | provider: Logger.MetadataProvider?, 76 | explicit: Logger.Metadata?, 77 | source: String, 78 | file: String, 79 | function: String, 80 | line: UInt 81 | ) -> Logger.Metadata { 82 | var metadata = base 83 | let provided = provider?.get() ?? [:] 84 | 85 | if !provided.isEmpty { 86 | metadata.merge(provided, uniquingKeysWith: { _, provided in provided }) 87 | } 88 | 89 | if let explicit = explicit, !explicit.isEmpty { 90 | metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit }) 91 | } 92 | 93 | // The human-readable message string for this entry 94 | metadata["MESSAGE"] = .string(message.description) 95 | 96 | // A priority value between 0 ("emerg") and 7 ("debug") 97 | metadata["PRIORITY"] = .string("\(level.syslogPriority)") 98 | 99 | // The code location generating this message, if known. 100 | metadata["CODE_FILE"] = .string(file) 101 | metadata["CODE_FUNC"] = .string(function) 102 | metadata["CODE_LINE"] = .string("\(line)") 103 | 104 | // The name of a unit. 105 | metadata["UNIT"] = .string(source) 106 | metadata["SYSLOG_IDENTIFIER"] = .string(label) 107 | 108 | return metadata 109 | } 110 | #endif 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Systemd/SystemdNotifier.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import CSystemd 3 | #endif 4 | 5 | /// Enumeration for state strings that can be sent to systemd. 6 | public enum ServiceState: String { 7 | /// Send when the service is "ready" or in normal operation state. 8 | case Ready = "READY=1" 9 | /// Send when the application is exiting, such as during graceful shutdown. 10 | case Stopping = "STOPPING=1" 11 | /// Send to kick the systemd watchdog timer. Only useful if `WatchdogSec` is configured. 12 | case Watchdog = "WATCHDOG=1" 13 | } 14 | 15 | /// Send notifications to systemd. 16 | public struct SystemdNotifier { 17 | public init() {} 18 | 19 | /// Send a notification to systemd. 20 | /// 21 | /// For now, all that is supported is calling `sd_notify` to send notifications to systemd. 22 | /// 23 | /// - Parameter state: the `ServiceState` string to send in the notification. 24 | public func notify(_ state: ServiceState) { 25 | #if os(Linux) 26 | sd_notify(0, state.rawValue) 27 | #endif 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Systemd/Utility.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Glibc) 2 | import Glibc 3 | 4 | /// Compute the prefix sum of `seq`. 5 | private func scan(_ seq: S, _ initial: U, _ combine: (U, S.Element) -> U) -> [U] { 6 | var result: [U] = [] 7 | result.reserveCapacity(seq.underestimatedCount) 8 | var runningResult = initial 9 | for element in seq { 10 | runningResult = combine(runningResult, element) 11 | result.append(runningResult) 12 | } 13 | return result 14 | } 15 | 16 | func withArrayOfIovecs(_ args: [String], _ body: ([iovec]) -> R) -> R { 17 | let argsCounts = Array(args.map { $0.utf8.count + 1 }) 18 | let argsOffsets = [0] + scan(argsCounts, 0, +) 19 | let argsBufferSize = argsOffsets.last! 20 | var argsBuffer: [UInt8] = [] 21 | argsBuffer.reserveCapacity(argsBufferSize) 22 | for arg in args { 23 | argsBuffer.append(contentsOf: arg.utf8) 24 | argsBuffer.append(0) 25 | } 26 | return argsBuffer.withUnsafeMutableBufferPointer { 27 | argsBuffer in 28 | let ptr = UnsafeMutableRawPointer(argsBuffer.baseAddress!).bindMemory( 29 | to: CChar.self, capacity: argsBuffer.count 30 | ) 31 | let iovecs: [iovec] = zip(argsCounts, argsOffsets).map { count, offset in 32 | iovec(iov_base: ptr + offset, iov_len: count - 1) 33 | } 34 | return body(iovecs) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/SystemdLifecycle/SystemdService.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import ServiceLifecycle 3 | import Systemd 4 | 5 | /// Simple service to send standard systemd notifications as part of the application lifecycle. 6 | /// 7 | /// This struct is implemented as a `ServiceLifecycle` service to be able to be run as part of 8 | /// the service group and report that the application is `Ready`. Then, if the watchdog is enabled, 9 | /// it can automatically send the `Watchdog` notification to systemd on the recommended interval. 10 | /// Finally, during graceful shutdown, the `Stopping` notification is sent to systemd to let it 11 | /// know that the application is shutting down. 12 | /// 13 | @available(iOS 16.0, tvOS 16.0, watchOS 9.0, *) 14 | public struct SystemdService: Service { 15 | public init() {} 16 | 17 | /// Run the service. 18 | public func run() async { 19 | let notifier = SystemdNotifier() 20 | 21 | // Send ready signal at startup 22 | notifier.notify(ServiceState.Ready) 23 | 24 | // Run the task until cancelled 25 | let watchdogEnabled = SystemdHelpers.watchdogEnabled 26 | let interval = SystemdHelpers.watchdogRecommendedNotifyInterval ?? .seconds(3600) 27 | for await _ in AsyncTimerSequence(interval: interval, clock: .continuous).cancelOnGracefulShutdown() { 28 | if watchdogEnabled { 29 | notifier.notify(ServiceState.Watchdog) 30 | } 31 | } 32 | 33 | // Notify of stopping before exiting 34 | notifier.notify(ServiceState.Stopping) 35 | } 36 | } 37 | --------------------------------------------------------------------------------