diff --git a/Sources/Lifecycle/Lifecycle.swift b/Sources/Lifecycle/Lifecycle.swift index 95a1d1d..3ec874b 100644 --- a/Sources/Lifecycle/Lifecycle.swift +++ b/Sources/Lifecycle/Lifecycle.swift @@ -105,25 +105,14 @@ public struct ServiceLifecycle { /// - parameters: /// - callback: The handler which is called after the start operation completes. The parameter will be `nil` on success and contain the `Error` otherwise. public func start(_ callback: @escaping (Error?) -> Void) { - self.configuration.shutdownSignal?.forEach { signal in - self.lifecycle.log("setting up shutdown hook on \(signal)") - let signalSource = ServiceLifecycle.trap(signal: signal, handler: { signal in - self.lifecycle.log("intercepted signal: \(signal)") - self.shutdown() - }) - self.lifecycle.shutdownGroup.notify(queue: .global()) { - signalSource.cancel() - } - } + self.setupShutdownHook() self.lifecycle.start(on: self.configuration.callbackQueue, callback) } /// Starts the provided `LifecycleItem` array and waits (blocking) until a shutdown `Signal` is captured or `shutdown` is called on another thread. /// Startup is performed in the order of items provided. - /// - /// - parameters: - /// - configuration: Defines lifecycle `Configuration` public func startAndWait() throws { + self.setupShutdownHook() try self.lifecycle.startAndWait(on: self.configuration.callbackQueue) } @@ -140,6 +129,19 @@ public struct ServiceLifecycle { public func wait() { self.lifecycle.wait() } + + private func setupShutdownHook() { + self.configuration.shutdownSignal?.forEach { signal in + self.lifecycle.log("setting up shutdown hook on \(signal)") + let signalSource = ServiceLifecycle.trap(signal: signal, handler: { signal in + self.lifecycle.log("intercepted signal: \(signal)") + self.shutdown() + }) + self.lifecycle.shutdownGroup.notify(queue: .global()) { + signalSource.cancel() + } + } + } } extension ServiceLifecycle { diff --git a/Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift b/Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift index a1a0993..5a4c05e 100644 --- a/Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift +++ b/Tests/LifecycleTests/ServiceLifecycleTests+XCTest.swift @@ -28,6 +28,7 @@ extension ServiceLifecycleTests { ("testStartThenShutdown", testStartThenShutdown), ("testShutdownWithSignal", testShutdownWithSignal), ("testStartAndWait", testStartAndWait), + ("testStartAndWaitShutdownWithSignal", testStartAndWaitShutdownWithSignal), ("testBadStartAndWait", testBadStartAndWait), ("testNesting", testNesting), ("testNesting2", testNesting2), diff --git a/Tests/LifecycleTests/ServiceLifecycleTests.swift b/Tests/LifecycleTests/ServiceLifecycleTests.swift index 148e0af..03f8c00 100644 --- a/Tests/LifecycleTests/ServiceLifecycleTests.swift +++ b/Tests/LifecycleTests/ServiceLifecycleTests.swift @@ -91,6 +91,55 @@ final class ServiceLifecycleTests: XCTestCase { XCTAssertEqual(item.state, .shutdown, "expected item to be shutdown") } + func testStartAndWaitShutdownWithSignal() { + if ProcessInfo.processInfo.environment["SKIP_SIGNAL_TEST"].flatMap(Bool.init) ?? false { + print("skipping testStartAndWaitShutdownWithSignal") + return + } + + class Item: LifecycleTask { + private let semaphore: DispatchSemaphore + var state = State.idle + + init(_ semaphore: DispatchSemaphore) { + self.semaphore = semaphore + } + + var label: String { + return "\(self)" + } + + func start(_ callback: (Error?) -> Void) { + self.state = .started + self.semaphore.signal() + callback(nil) + } + + func shutdown(_ callback: (Error?) -> Void) { + self.state = .shutdown + callback(nil) + } + + enum State { + case idle + case started + case shutdown + } + } + + let signal = ServiceLifecycle.Signal.ALRM + let lifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [signal])) + let semaphore = DispatchSemaphore(value: 0) + DispatchQueue(label: "test").asyncAfter(deadline: .now() + 0.1) { + semaphore.wait() + kill(getpid(), signal.rawValue) + } + let item = Item(semaphore) + lifecycle.register(item) + XCTAssertNoThrow(try lifecycle.startAndWait()) + XCTAssertEqual(item.state, .shutdown, "expected item to be shutdown") + } + func testBadStartAndWait() { class BadItem: LifecycleTask { var label: String { diff --git a/docker/Dockerfile b/docker/Dockerfile index cfd977c..e64f39a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,7 +18,7 @@ RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools # ruby and jazzy for docs generation RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev -RUN gem install jazzy --no-ri --no-rdoc +RUN if [ "${ubuntu_version}" != "xenial" ] ; then gem install jazzy --no-ri --no-rdoc ; fi # tools RUN mkdir -p $HOME/.tools diff --git a/docker/docker-compose.1804.50.yaml b/docker/docker-compose.1804.50.yaml index b48458c..64d7bb6 100644 --- a/docker/docker-compose.1804.50.yaml +++ b/docker/docker-compose.1804.50.yaml @@ -11,6 +11,8 @@ services: test: image: swift-service-lifecycle:18.04-5.0 - + environment: + - SKIP_SIGNAL_TEST=true + shell: image: swift-service-lifecycle:18.04-5.0 diff --git a/docker/docker-compose.1804.52.yaml b/docker/docker-compose.1804.52.yaml index ecd482d..05fb6b2 100644 --- a/docker/docker-compose.1804.52.yaml +++ b/docker/docker-compose.1804.52.yaml @@ -11,6 +11,8 @@ services: test: image: swift-service-lifecycle:18.04-5.2 + environment: + - SKIP_SIGNAL_TEST=true shell: image: swift-service-lifecycle:18.04-5.2 diff --git a/docker/docker-compose.1804.53.yaml b/docker/docker-compose.1804.53.yaml index 0277558..6280d17 100644 --- a/docker/docker-compose.1804.53.yaml +++ b/docker/docker-compose.1804.53.yaml @@ -10,6 +10,8 @@ services: test: image: swift-service-lifecycle:18.04-5.3 + environment: + - SKIP_SIGNAL_TEST=true shell: image: swift-service-lifecycle:18.04-5.3