diff --git a/Sources/Events/EventObserver.swift b/Sources/Events/EventObserver.swift index 91d96d7..5217824 100644 --- a/Sources/Events/EventObserver.swift +++ b/Sources/Events/EventObserver.swift @@ -3,21 +3,16 @@ import Cocoa class EventObserver: EventListener, EventProvider { var listener: EventListener? - var sources: [EventSource] = [] + let sources: [EventSource] init(_ sources: [EventSource]) { + self.sources = sources for var source in sources { source.listener = self } - self.sources = sources } func handle(_ event: Event) { - var message = "\(event.source.cyan) emitted \(event.kind.blue) event" - if !event.target.isEmpty { - message += " with \(event.target.yellow)" - } - logger.debug(message) guard let listener else { return } diff --git a/Sources/RunOn.swift b/Sources/RunOn.swift index 673046a..ad72714 100644 --- a/Sources/RunOn.swift +++ b/Sources/RunOn.swift @@ -2,7 +2,10 @@ import ArgumentParser import ServiceManagement let kAppId = "co.myrt.runon" -var logger = Logger(level: .error) +var logger = Logger(config: .init( + level: .error, + showTimestamp: true +)) @main struct RunOn: ParsableCommand { diff --git a/Sources/Runner/ActionGroupQueue.swift b/Sources/Runner/ActionGroupQueue.swift new file mode 100644 index 0000000..2fa7603 --- /dev/null +++ b/Sources/Runner/ActionGroupQueue.swift @@ -0,0 +1,42 @@ +import Foundation +import Shellac + +enum ActionError: Error { + /// Queue is currently busy + case busy +} + +/// ActionGroupQueue represents a queue of actions. +/// Helps ensure that groups of commands are executed correctly +/// and allows for fine tuning of debounce. +class ActionGroupQueue { + let name: String + let interval: TimeInterval + + private let semaphore = DispatchSemaphore(value: 1) + + init(name: String, interval: TimeInterval? = nil) { + self.name = name + guard let interval = interval else { + self.interval = 0 + return + } + self.interval = interval + } + + /// Run action and return output + func run(_ action: Action) throws -> String { + if semaphore.wait(timeout: .now()) == .timedOut { + throw ActionError.busy + } + let output = try shell(with: action.commands, timeout: action.timeout) + if interval == 0 { + semaphore.signal() + return output + } + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + self.semaphore.signal() + } + return output + } +} diff --git a/Sources/Runner/ActionLogger.swift b/Sources/Runner/ActionLogger.swift new file mode 100644 index 0000000..1d81316 --- /dev/null +++ b/Sources/Runner/ActionLogger.swift @@ -0,0 +1,55 @@ +import Shellac + +struct ActionRunnerLogger { + private let logger: Logger + + init(with logger: Logger) { + self.logger = logger + } + + func actionNotFound() { + self.logger.info("action not found, skipping") + } + + func queueNotFound(_ name: String) { + self.logger.error("queue not found: \(name)") + } + + func actionSuccess(_ output: String) { + self.logger.info("command successfully finished".green) + if !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + self.logger.debug("output: \(output)") + } + } + + func actionStarted(_ action: Action) { + var message = "starting action \(action.source.cyan):\(action.kind.blue)" + if let target = action.target { + message += " with \(target.yellow)" + } + message += " on \(action.group.magenta)" + self.logger.info(message) + } + + func actionFailed(_ error: Error) { + if let error = error as? ShellError { + self.logger.error("the process exited with a non-zero status code: \(error.code).") + if !error.error.isEmpty { + self.logger.error("output: \(error.error)") + } + } else if let error = error as? ActionError { + switch error { + case .busy: + self.logger.info("queue busy, skipping") + } + } else { + self.logger.error(String(describing: error)) + } + } + + func eventReceived(_ event: Event) { + self.logger.info("received event \(event.source.cyan):\(event.kind.blue) with \(event.target.yellow)") + } +} + +let kActionLogger = ActionRunnerLogger(with: logger.child(prefix: "Runner")) diff --git a/Sources/Runner/ActionQueue.swift b/Sources/Runner/ActionQueue.swift deleted file mode 100644 index 9bf4643..0000000 --- a/Sources/Runner/ActionQueue.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import Shellac - -class ActionQueue { - var name: String - var isRunning = false - var interval: TimeInterval? - - init(name: String, interval: TimeInterval? = nil) { - self.name = name - self.interval = interval - } - - func run(_ action: Action) { - guard let debounceInterval = self.interval else { - self.runAction(action) - return - } - if isRunning { - logger.debug("debounced action \(formatMessage(action))") - return - } - isRunning = true - logger.debug("lock \(self.name.magenta) queue") - self.runAction(action) - DispatchQueue.main.asyncAfter(deadline: .now() + debounceInterval) { - logger.debug("unlock \(self.name.magenta) queue") - self.isRunning = false - } - } - - private func formatMessage(_ action: Action) -> String { - let event = "\(action.source.cyan):\(action.kind.blue)" - guard let target = action.target else { - return event - } - return "on \(name.magenta) - \(event) with \(target.yellow)" - } - - private func runAction(_ action: Action) { - do { - logger.info("running \(formatMessage(action)) action") - let output = try shell(with: action.commands, timeout: action.timeout) - logger.info("command successfully finished".green) - if !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - logger.debug("output:\n\(output)") - } - } catch { - if let error = error as? ShellError { - logger.error("The process exited with a non-zero status code: \(error.code).") - if !error.error.isEmpty { - logger.error("Message: \(error.error)") - } - } else { - logger.error(String(describing: error)) - } - } - } -} diff --git a/Sources/Runner/ActionRunner.swift b/Sources/Runner/ActionRunner.swift index ed73ca7..e363c6c 100644 --- a/Sources/Runner/ActionRunner.swift +++ b/Sources/Runner/ActionRunner.swift @@ -1,14 +1,16 @@ import Foundation import Shellac +typealias ActionGroupQueueMap = [String: ActionGroupQueue] + class ActionRunner: EventListener { let handler: ConfigHandler - var queues: [String: ActionQueue] = [:] + var queues: ActionGroupQueueMap = [:] init(with handler: ConfigHandler) { self.handler = handler for (name, group) in handler.groupMap { - queues[name] = ActionQueue( + queues[name] = ActionGroupQueue( name: name, interval: group.debounce ) @@ -16,14 +18,25 @@ class ActionRunner: EventListener { } func handle(_ event: Event) { + kActionLogger.eventReceived(event) guard let action = handler.findAction( source: event.source, kind: event.kind, target: event.target ) else { - logger.debug("handler not found") + kActionLogger.actionNotFound() return } - queues[action.group]?.run(action) + guard let queue = queues[action.group] else { + kActionLogger.queueNotFound(action.group) + return + } + do { + kActionLogger.actionStarted(action) + let output = try queue.run(action) + kActionLogger.actionSuccess(output) + } catch { + kActionLogger.actionFailed(error) + } } }