source: launchers/macosx/I2PLauncher/Utils/LaunchAgentManager.swift @ 1a00f73

Last change on this file since 1a00f73 was 1a00f73, checked in by meeh <meeh@…>, 20 months ago

OSX Launcher: Extended start/stop/load/unload to be able to set terminationHandler for more reliable execution.

  • Property mode set to 100644
File size: 5.9 KB
Line 
1//
2//  LaunchAgentManager.swift
3//  I2PLauncher
4//
5//  Created by Mikal Villa on 07/10/2018.
6//  Copyright © 2018 The I2P Project. All rights reserved.
7//
8
9import Foundation
10
11
12public enum LaunchAgentManagerError: Swift.Error {
13  case urlNotSet(label: String)
14 
15  public var localizedDescription: String {
16    switch self {
17    case .urlNotSet(let label):
18      return "The URL is not set for agent \(label)"
19    }
20  }
21}
22
23public class LaunchAgentManager {
24  public static let shared = LaunchAgentManager()
25 
26  static let launchctl = "/bin/launchctl"
27 
28  var lastState: AgentStatus?
29 
30  let encoder = PropertyListEncoder()
31  let decoder = PropertyListDecoder()
32 
33  init() {
34    encoder.outputFormat = .xml
35  }
36 
37  func launchAgentsURL() throws -> URL {
38    let library = try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
39   
40    return library.appendingPathComponent("LaunchAgents")
41  }
42 
43  public func read(agent called: String) throws -> LaunchAgent {
44    let url = try launchAgentsURL().appendingPathComponent(called)
45   
46    return try read(from: url)
47  }
48 
49  public func read(from url: URL) throws -> LaunchAgent {
50    return try decoder.decode(LaunchAgent.self, from: Data(contentsOf: url))
51  }
52 
53  public func write(_ agent: LaunchAgent, called: String) throws {
54    let url = try launchAgentsURL().appendingPathComponent(called)
55   
56    try write(agent, to: url)
57  }
58 
59  public func write(_ agent: LaunchAgent, to url: URL) throws {
60    try encoder.encode(agent).write(to: url)
61   
62    agent.url = url
63  }
64 
65  public func setURL(for agent: LaunchAgent) throws {
66    let contents = try FileManager.default.contentsOfDirectory(
67      at: try launchAgentsURL(),
68      includingPropertiesForKeys: nil,
69      options: [.skipsPackageDescendants, .skipsHiddenFiles, .skipsSubdirectoryDescendants]
70    )
71   
72    contents.forEach { url in
73      let testAgent = try? self.read(from: url)
74     
75      if agent.label == testAgent?.label {
76        agent.url = url
77        return
78      }
79    }
80   
81   
82  }
83 
84}
85
86extension LaunchAgentManager {
87 
88  /// Run `launchctl start` on the agent
89  ///
90  /// Check the status of the job with `.status(_: LaunchAgent)`
91  public func start(_ agent: LaunchAgent, _ termHandler: ((Process) -> Void)? = nil ) {
92    let arguments = ["start", agent.label]
93    let proc = Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
94    if ((termHandler) != nil) {
95      proc.terminationHandler = termHandler
96    }
97  }
98 
99  /// Run `launchctl stop` on the agent
100  ///
101  /// Check the status of the job with `.status(_: LaunchAgent)`
102  public func stop(_ agent: LaunchAgent, _ termHandler: ((Process) -> Void)? = nil ) {
103    let arguments = ["stop", agent.label]
104    let proc = Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
105    if ((termHandler) != nil) {
106      proc.terminationHandler = termHandler
107    }
108  }
109 
110  /// Run `launchctl load` on the agent
111  ///
112  /// Check the status of the job with `.status(_: LaunchAgent)`
113  public func load(_ agent: LaunchAgent, _ termHandler: ((Process) -> Void)? = nil ) throws {
114    guard let agentURL = agent.url else {
115      throw LaunchAgentManagerError.urlNotSet(label: agent.label)
116    }
117   
118    let arguments = ["load", agentURL.path]
119    let proc = Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
120    if ((termHandler) != nil) {
121      proc.terminationHandler = termHandler
122    }
123  }
124 
125  /// Run `launchctl unload` on the agent
126  ///
127  /// Check the status of the job with `.status(_: LaunchAgent)`
128  public func unload(_ agent: LaunchAgent, _ termHandler: ((Process) -> Void)? = nil ) throws {
129    guard let agentURL = agent.url else {
130      throw LaunchAgentManagerError.urlNotSet(label: agent.label)
131    }
132   
133    let arguments = ["unload", agentURL.path]
134    let proc = Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
135    if ((termHandler) != nil) {
136      proc.terminationHandler = termHandler
137    }
138  }
139 
140  /// Retreives the status of the LaunchAgent from `launchctl`
141  ///
142  /// - Returns: the agent's status
143  public func status(_ agent: LaunchAgent) -> AgentStatus {
144   
145    let launchctlTask = Process()
146    let grepTask = Process()
147    let cutTask = Process()
148   
149    launchctlTask.launchPath = "/bin/launchctl"
150    launchctlTask.arguments = ["list"]
151   
152    grepTask.launchPath = "/usr/bin/grep"
153    grepTask.arguments = [agent.label]
154   
155    cutTask.launchPath = "/usr/bin/cut"
156    cutTask.arguments = ["-f1"]
157   
158    let pipeLaunchCtlToGrep = Pipe()
159    launchctlTask.standardOutput = pipeLaunchCtlToGrep
160    grepTask.standardInput = pipeLaunchCtlToGrep
161   
162    let pipeGrepToCut = Pipe()
163    grepTask.standardOutput = pipeGrepToCut
164    cutTask.standardInput = pipeGrepToCut
165   
166    let pipeCutToFile = Pipe()
167    cutTask.standardOutput = pipeCutToFile
168   
169    let fileHandle: FileHandle = pipeCutToFile.fileHandleForReading as FileHandle
170   
171    launchctlTask.launch()
172    grepTask.launch()
173    cutTask.launch()
174   
175   
176    let data = fileHandle.readDataToEndOfFile()
177    let stringResult = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? ""
178   
179    let em = RouterManager.shared().eventManager
180   
181    switch stringResult {
182    case "-":
183      if (self.lastState != AgentStatus.loaded) {
184        self.lastState = AgentStatus.loaded
185        em.trigger(eventName: "launch_agent_loaded")
186      }
187
188      return .loaded
189    case "":
190      if (self.lastState != AgentStatus.unloaded) {
191        self.lastState = AgentStatus.unloaded
192        em.trigger(eventName: "launch_agent_unloaded")
193      }
194      return .unloaded
195    default:
196      if (self.lastState != AgentStatus.running(pid: Int(stringResult)!)) {
197        self.lastState = AgentStatus.running(pid: Int(stringResult)!)
198        em.trigger(eventName: "launch_agent_running")
199      }
200      return .running(pid: Int(stringResult)!)
201    }
202  }
203}
Note: See TracBrowser for help on using the repository browser.