source: launchers/macosx/I2PLauncher/Utils/LaunchAgentManager.swift @ 45b4f42

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

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.

  • Property mode set to 100644
File size: 5.4 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) {
92    let arguments = ["start", agent.label]
93    Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
94  }
95 
96  /// Run `launchctl stop` on the agent
97  ///
98  /// Check the status of the job with `.status(_: LaunchAgent)`
99  public func stop(_ agent: LaunchAgent) {
100    let arguments = ["stop", agent.label]
101    Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
102  }
103 
104  /// Run `launchctl load` on the agent
105  ///
106  /// Check the status of the job with `.status(_: LaunchAgent)`
107  public func load(_ agent: LaunchAgent) throws {
108    guard let agentURL = agent.url else {
109      throw LaunchAgentManagerError.urlNotSet(label: agent.label)
110    }
111   
112    let arguments = ["load", agentURL.path]
113    Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
114  }
115 
116  /// Run `launchctl unload` on the agent
117  ///
118  /// Check the status of the job with `.status(_: LaunchAgent)`
119  public func unload(_ agent: LaunchAgent) throws {
120    guard let agentURL = agent.url else {
121      throw LaunchAgentManagerError.urlNotSet(label: agent.label)
122    }
123   
124    let arguments = ["unload", agentURL.path]
125    Process.launchedProcess(launchPath: LaunchAgentManager.launchctl, arguments: arguments)
126  }
127 
128  /// Retreives the status of the LaunchAgent from `launchctl`
129  ///
130  /// - Returns: the agent's status
131  public func status(_ agent: LaunchAgent) -> AgentStatus {
132   
133    let launchctlTask = Process()
134    let grepTask = Process()
135    let cutTask = Process()
136   
137    launchctlTask.launchPath = "/bin/launchctl"
138    launchctlTask.arguments = ["list"]
139   
140    grepTask.launchPath = "/usr/bin/grep"
141    grepTask.arguments = [agent.label]
142   
143    cutTask.launchPath = "/usr/bin/cut"
144    cutTask.arguments = ["-f1"]
145   
146    let pipeLaunchCtlToGrep = Pipe()
147    launchctlTask.standardOutput = pipeLaunchCtlToGrep
148    grepTask.standardInput = pipeLaunchCtlToGrep
149   
150    let pipeGrepToCut = Pipe()
151    grepTask.standardOutput = pipeGrepToCut
152    cutTask.standardInput = pipeGrepToCut
153   
154    let pipeCutToFile = Pipe()
155    cutTask.standardOutput = pipeCutToFile
156   
157    let fileHandle: FileHandle = pipeCutToFile.fileHandleForReading as FileHandle
158   
159    launchctlTask.launch()
160    grepTask.launch()
161    cutTask.launch()
162   
163   
164    let data = fileHandle.readDataToEndOfFile()
165    let stringResult = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? ""
166   
167    let em = RouterManager.shared().eventManager
168   
169    switch stringResult {
170    case "-":
171      if (self.lastState != AgentStatus.loaded) {
172        self.lastState = AgentStatus.loaded
173        em.trigger(eventName: "launch_agent_loaded")
174      }
175
176      return .loaded
177    case "":
178      if (self.lastState != AgentStatus.unloaded) {
179        self.lastState = AgentStatus.unloaded
180        em.trigger(eventName: "launch_agent_unloaded")
181      }
182      return .unloaded
183    default:
184      if (self.lastState != AgentStatus.running(pid: Int(stringResult)!)) {
185        self.lastState = AgentStatus.running(pid: Int(stringResult)!)
186        em.trigger(eventName: "launch_agent_running")
187      }
188      return .running(pid: Int(stringResult)!)
189    }
190  }
191}
Note: See TracBrowser for help on using the repository browser.