Files
i2p.i2p/launchers/macosx/I2PLauncher/Utils/LaunchAgentManager.swift
meeh 45b4f426a8 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.
2018-10-11 16:55:07 +00:00

192 lines
5.4 KiB
Swift

//
// 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)!)
}
}
}