OSX Launcher: Big rewrite of swift code where it now has the capability of creating services.

The router management has been much easier with this approach as it uses launchd to do the dirty work.
This code also uses java_home as a wrapper instead of locating the java binary by itself. This also contribute to the improvements.
This commit is contained in:
meeh
2018-10-11 16:55:07 +00:00
parent 51cbd8ef87
commit 45b4f426a8
3 changed files with 384 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
//
// LaunchAgent+Status.swift
// I2PLauncher
//
// Created by Mikal Villa on 05/10/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public enum AgentStatus: Equatable {
case running(pid: Int)
case loaded
case unloaded
public static func ==(lhs: AgentStatus, rhs: AgentStatus) -> Bool {
switch (lhs, rhs) {
case ( let .running(lhpid), let .running(rhpid) ):
return lhpid == rhpid
case (.loaded, .loaded):
return true
case (.unloaded, .unloaded):
return true
default:
return false
}
}
}
extension LaunchAgent {
/// Run `launchctl start` on the agent
///
/// Check the status of the job with `.status()`
public func start() {
LaunchAgentManager.shared.start(self)
}
/// Run `launchctl stop` on the agent
///
/// Check the status of the job with `.status()`
public func stop() {
LaunchAgentManager.shared.stop(self)
}
/// Run `launchctl load` on the agent
///
/// Check the status of the job with `.status()`
public func load() throws {
try LaunchAgentManager.shared.load(self)
}
/// Run `launchctl unload` on the agent
///
/// Check the status of the job with `.status()`
public func unload() throws {
try LaunchAgentManager.shared.unload(self)
}
/// Retreives the status of the LaunchAgent from `launchctl`
///
/// - Returns: the agent's status
public func status() -> AgentStatus {
return LaunchAgentManager.shared.status(self)
}
}

View File

@@ -0,0 +1,124 @@
//
// LaunchAgent.swift
// I2PLauncher
//
// Created by Mikal Villa on 05/10/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public enum ProcessType: String, Codable {
case standard = "Standard"
case background = "Background"
case adaptive = "Adaptive"
case interactive = "Interactive"
}
public class LaunchAgent: Codable {
public var url: URL? = nil
// Basic Properties
public var label: String
public var disabled: Bool? = nil
public var enableGlobbing: Bool? = nil
public var program: String? = nil {
didSet {
if program != nil {
programArguments = nil
}
}
}
public var programArguments: [String]? = nil {
didSet {
guard let args = programArguments else {
return
}
if args.count == 1 {
self.program = args.first
programArguments = nil
} else {
program = nil
}
}
}
public var processType: ProcessType? = nil
// Program
public var workingDirectory: String? = nil
public var standardOutPath: String? = nil
public var standardErrorPath: String? = nil
public var environmentVariables: [String: String]? = nil
// Run Conditions
public var runAtLoad: Bool? = nil
public var startInterval: Int? = nil
public var onDemand: Bool? = nil
public var keepAlive: Bool? = nil
public var watchPaths: [String]? = nil
// Security
public var umask: Int? = nil
// System Daemon Security
public var groupName: String? = nil
public var userName: String? = nil
public var rootDirectory: String? = nil
// Run Constriants
public var launchOnlyOnce: Bool? = nil
public var limitLoadToSessionType: [String]? = nil
public init(label: String, program: [String]) {
self.label = label
if program.count == 1 {
self.program = program.first
} else {
self.programArguments = program
}
}
public convenience init(label: String, program: String...) {
self.init(label: label, program: program)
}
public enum CodingKeys: String, CodingKey {
case label = "Label"
case disabled = "Disabled"
case program = "Program"
case programArguments = "ProgramArguments"
// Program
case workingDirectory = "WorkingDirectory"
case standardOutPath = "StandardOutPath"
case standardErrorPath = "StandardErrorPath"
case environmentVariables = "EnvironmentVariables"
// Run Conditions
case runAtLoad = "RunAtLoad"
case startInterval = "StartInterval"
case onDemand = "OnDemand"
case keepAlive = "KeepAlive"
case watchPaths = "WatchPaths"
// Security
case umask = "Umask"
case groupName = "GroupName"
case userName = "UserName"
case rootDirectory = "RootDirectory"
// Run Constriants
case launchOnlyOnce = "LaunchOnlyOnce"
case limitLoadToSessionType = "LimitLoadToSessionType"
// Process type
case processType = "ProcessType"
}
}

View File

@@ -0,0 +1,191 @@
//
// LaunchAgentManager.swift
// I2PLauncher
//
// Created by Mikal Villa on 07/10/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public enum LaunchAgentManagerError: Swift.Error {
case urlNotSet(label: String)
public var localizedDescription: String {
switch self {
case .urlNotSet(let label):
return "The URL is not set for agent \(label)"
}
}
}
public class LaunchAgentManager {
public static let shared = LaunchAgentManager()
static let launchctl = "/bin/launchctl"
var lastState: AgentStatus?
let encoder = PropertyListEncoder()
let decoder = PropertyListDecoder()
init() {
encoder.outputFormat = .xml
}
func launchAgentsURL() throws -> URL {
let library = try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
return library.appendingPathComponent("LaunchAgents")
}
public func read(agent called: String) throws -> LaunchAgent {
let url = try launchAgentsURL().appendingPathComponent(called)
return try read(from: url)
}
public func read(from url: URL) throws -> LaunchAgent {
return try decoder.decode(LaunchAgent.self, from: Data(contentsOf: url))
}
public func write(_ agent: LaunchAgent, called: String) throws {
let url = try launchAgentsURL().appendingPathComponent(called)
try write(agent, to: url)
}
public func write(_ agent: LaunchAgent, to url: URL) throws {
try encoder.encode(agent).write(to: url)
agent.url = url
}
public func setURL(for agent: LaunchAgent) throws {
let contents = try FileManager.default.contentsOfDirectory(
at: try launchAgentsURL(),
includingPropertiesForKeys: nil,
options: [.skipsPackageDescendants, .skipsHiddenFiles, .skipsSubdirectoryDescendants]
)
contents.forEach { url in
let testAgent = try? self.read(from: url)
if agent.label == testAgent?.label {
agent.url = url
return
}
}
}
}
extension LaunchAgentManager {
/// Run `launchctl start` on the agent
///
/// Check the status of the job with `.status(_: LaunchAgent)`
public func start(_ agent: LaunchAgent) {
let arguments = ["start", agent.label]
Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
}
/// Run `launchctl stop` on the agent
///
/// Check the status of the job with `.status(_: LaunchAgent)`
public func stop(_ agent: LaunchAgent) {
let arguments = ["stop", agent.label]
Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
}
/// Run `launchctl load` on the agent
///
/// Check the status of the job with `.status(_: LaunchAgent)`
public func load(_ agent: LaunchAgent) throws {
guard let agentURL = agent.url else {
throw LaunchAgentManagerError.urlNotSet(label: agent.label)
}
let arguments = ["load", agentURL.path]
Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
}
/// Run `launchctl unload` on the agent
///
/// Check the status of the job with `.status(_: LaunchAgent)`
public func unload(_ agent: LaunchAgent) throws {
guard let agentURL = agent.url else {
throw LaunchAgentManagerError.urlNotSet(label: agent.label)
}
let arguments = ["unload", agentURL.path]
Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
}
/// Retreives the status of the LaunchAgent from `launchctl`
///
/// - Returns: the agent's status
public func status(_ agent: LaunchAgent) -> AgentStatus {
let launchctlTask = Process()
let grepTask = Process()
let cutTask = Process()
launchctlTask.launchPath = "/bin/launchctl"
launchctlTask.arguments = ["list"]
grepTask.launchPath = "/usr/bin/grep"
grepTask.arguments = [agent.label]
cutTask.launchPath = "/usr/bin/cut"
cutTask.arguments = ["-f1"]
let pipeLaunchCtlToGrep = Pipe()
launchctlTask.standardOutput = pipeLaunchCtlToGrep
grepTask.standardInput = pipeLaunchCtlToGrep
let pipeGrepToCut = Pipe()
grepTask.standardOutput = pipeGrepToCut
cutTask.standardInput = pipeGrepToCut
let pipeCutToFile = Pipe()
cutTask.standardOutput = pipeCutToFile
let fileHandle: FileHandle = pipeCutToFile.fileHandleForReading as FileHandle
launchctlTask.launch()
grepTask.launch()
cutTask.launch()
let data = fileHandle.readDataToEndOfFile()
let stringResult = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? ""
let em = RouterManager.shared().eventManager
switch stringResult {
case "-":
if (self.lastState != AgentStatus.loaded) {
self.lastState = AgentStatus.loaded
em.trigger(eventName: "launch_agent_loaded")
}
return .loaded
case "":
if (self.lastState != AgentStatus.unloaded) {
self.lastState = AgentStatus.unloaded
em.trigger(eventName: "launch_agent_unloaded")
}
return .unloaded
default:
if (self.lastState != AgentStatus.running(pid: Int(stringResult)!)) {
self.lastState = AgentStatus.running(pid: Int(stringResult)!)
em.trigger(eventName: "launch_agent_running")
}
return .running(pid: Int(stringResult)!)
}
}
}