From 3988a8645dba315478d0f1c10a2a4ec5b1da6775 Mon Sep 17 00:00:00 2001
From: meeh <meeh@mail.i2p>
Date: Thu, 11 Oct 2018 16:59:59 +0000
Subject: [PATCH] OSX Launcher: major updates to the glue between 'backend' and
 GUI. Implemented the use of the new LaunchAgent classes

---
 .../routermgmt/RouterManager.swift            |  41 +++-
 .../RouterProcessStatus+ObjectiveC.swift      |   4 +-
 .../routermgmt/RouterProcessStatus.swift      |   8 +-
 .../I2PLauncher/routermgmt/RouterRunner.swift | 228 +++++++++++++++---
 4 files changed, 225 insertions(+), 56 deletions(-)

diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterManager.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterManager.swift
index 8715542e34..e274d2017e 100644
--- a/launchers/macosx/I2PLauncher/routermgmt/RouterManager.swift
+++ b/launchers/macosx/I2PLauncher/routermgmt/RouterManager.swift
@@ -19,34 +19,42 @@ class RouterManager : NSObject {
   
   // MARK: - Properties
   
-  static let packedVersion : String = "0.9.36"
+  static let packedVersion : String = "0.9.37"
   
   let eventManager = EventManager()
+  let routerRunner = RouterRunner()
   
   var logViewStorage: NSTextStorage?
   
   private static func handleRouterException(information:Any?) {
-    NSLog("event! - handle router exception")
-    NSLog(information as! String)
+    Logger.MLog(level:1,"event! - handle router exception")
+    Logger.MLog(level:1,information as! String)
   }
   private static func handleRouterStart(information:Any?) {
-    NSLog("event! - handle router start")
+    Logger.MLog(level:1,"event! - handle router start")
     RouterProcessStatus.routerStartedAt = Date()
     RouterProcessStatus.isRouterChildProcess = true
     RouterProcessStatus.isRouterRunning = true
   }
+  private static func handleRouterAlreadyStarted(information:Any?) {
+    Logger.MLog(level:1,"event! - handle router already started");
+  }
   private static func handleRouterStop(information:Any?) {
-    NSLog("event! - handle router stop")
+    Logger.MLog(level:1,"event! - handle router stop")
+    // TODO: Double check, check if pid stored exists
     RouterProcessStatus.routerStartedAt = nil
     RouterProcessStatus.isRouterChildProcess = false
     RouterProcessStatus.isRouterRunning = false
   }
   private static func handleRouterPid(information:Any?) {
-    Swift.print("event! - handle router pid: ", information ?? "")
+    Logger.MLog(level:1,"".appendingFormat("event! - handle router pid: ", information as! String!))
+    if (information != nil) {
+      let intPid = Int(information as! String)
+    }
   }
   private static func handleRouterVersion(information:Any?) {
     do {
-      Swift.print("event! - handle router version: ", information ?? "")
+      Logger.MLog(level:1, "".appendingFormat("event! - handle router version: ", information as! String!))
       guard let currentVersion : String = information as? String else {
         throw ErrorsInRouterMgmr.InvalidVersion
       }
@@ -54,11 +62,11 @@ class RouterManager : NSObject {
         throw ErrorsInRouterMgmr.InvalidVersion
       }
       if (packedVersion.compare(currentVersion, options: .numeric) == .orderedDescending) {
-        Swift.print("event! - router version: Packed version is newer, gonna re-deploy")
+        Logger.MLog(level:1,"event! - router version: Packed version is newer, gonna re-deploy")
         RouterManager.shared().eventManager.trigger(eventName: "router_must_upgrade", information: "got new version")
       } else {
-        Swift.print("event! - router version: No update needed")
-        RouterManager.shared().eventManager.trigger(eventName: "router_can_start", information: "all ok")
+        Logger.MLog(level:1,"event! - router version: No update needed")
+        RouterManager.shared().eventManager.trigger(eventName: "router_can_setup", information: "all ok")
       }
     } catch ErrorsInRouterMgmr.InvalidVersion {
       // This is most likely due to an earlier extract got killed halfway or something
@@ -72,8 +80,7 @@ class RouterManager : NSObject {
   }
   
   private static var sharedRouterManager: RouterManager = {
-    let inst = DetectJava()
-    let routerManager = RouterManager(detectJavaInstance: inst)
+    let routerManager = RouterManager(detectJavaInstance: DetectJava.shared())
     
     // Configuration
     // ...
@@ -84,6 +91,9 @@ class RouterManager : NSObject {
     routerManager.eventManager.listenTo(eventName: "router_pid", action: handleRouterPid)
     routerManager.eventManager.listenTo(eventName: "router_version", action: handleRouterVersion)
     routerManager.eventManager.listenTo(eventName: "router_exception", action: handleRouterException)
+    routerManager.eventManager.listenTo(eventName: "router_already_running", action: handleRouterAlreadyStarted)
+    routerManager.eventManager.listenTo(eventName: "router_can_start", action: routerManager.routerRunner.StartAgent)
+    routerManager.eventManager.listenTo(eventName: "router_can_setup", action: routerManager.routerRunner.SetupAgent)
     return routerManager
   }()
   
@@ -108,6 +118,13 @@ class RouterManager : NSObject {
   
   // MARK: - Accessors
   
+  static func logInfo(format: String, messages: String...) {
+    //SBridge.sharedInstance().logMessageWithFormat(0, format, messages)func k(_ x: Int32, _ params: String...) {
+    /*withVaList(messages) {
+      genericLogger(x, $0)
+    }*/
+  }
+  
   class func shared() -> RouterManager {
     return sharedRouterManager
   }
diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus+ObjectiveC.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus+ObjectiveC.swift
index 0c4c8c3f8d..a4f2b4868f 100644
--- a/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus+ObjectiveC.swift
+++ b/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus+ObjectiveC.swift
@@ -10,10 +10,10 @@ import Foundation
 
 extension RouterProcessStatus {
   
-  static func createNewRouterProcess(i2pPath: String, javaBinPath: String) {
+  static func createNewRouterProcess(i2pPath: String) {
     let timeWhenStarted = Date()
     RouterProcessStatus.routerStartedAt = timeWhenStarted
-    SBridge.sharedInstance().startupI2PRouter(i2pPath, javaBinPath: javaBinPath)
+    SBridge.sharedInstance().startupI2PRouter(i2pPath)
     RouterManager.shared().updateState()
   }
   static func shutdownRouterChildProcess() {
diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus.swift
index 29b343fa75..ca1abd0b83 100644
--- a/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus.swift
+++ b/launchers/macosx/I2PLauncher/routermgmt/RouterProcessStatus.swift
@@ -30,7 +30,11 @@ import AppKit
   }
   
   @objc func getJavaHome() -> String {
-    return RouterProcessStatus.knownJavaBinPath!
+    return DetectJava.shared().javaHome
+  }
+  
+  @objc func getJavaViaLibexec() -> Array<String> {
+    return DetectJava.shared().getJavaViaLibexecBin()
   }
   
   @objc func triggerEvent(en: String, details: String? = nil) {
@@ -47,10 +51,8 @@ extension RouterProcessStatus {
   static var isRouterChildProcess : Bool = (RouterManager.shared().getRouterTask() != nil)
   static var routerVersion : String? = Optional.none
   static var routerStartedAt : Date? = Optional.none
-  static var knownJavaBinPath : String? = Optional.none
   static var i2pDirectoryPath : String = NSHomeDirectory() + "/Library/I2P"
   
-  static var knownRouterSubTaskRef : I2PSubprocess? = Optional.none
   
 }
 
diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterRunner.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterRunner.swift
index 09d04db116..aa416f7709 100644
--- a/launchers/macosx/I2PLauncher/routermgmt/RouterRunner.swift
+++ b/launchers/macosx/I2PLauncher/routermgmt/RouterRunner.swift
@@ -8,62 +8,212 @@
 
 import Foundation
 
-class RouterRunner: NSObject, I2PSubprocess {
+class RouterRunner: NSObject {
   
-  var subprocessPath: String?
+  
+  var daemonPath: String?
   var arguments: String?
-  var timeWhenStarted: Date?
+  
+  static var launchAgent: LaunchAgent?
+  let routerStatus: RouterProcessStatus = RouterProcessStatus()
   
   var currentRunningProcess: Subprocess?
   var currentProcessResults: ExecutionResult?
   
-  func findJava() {
-    self.subprocessPath = RouterProcessStatus.knownJavaBinPath
-  }
+  let domainLabel = "net.i2p.macosx.I2PRouter"
+  
+  let plistName = "net.i2p.macosx.I2PRouterAgent.plist"
   
-  let defaultStartupFlags:[String] = [
-    "-Xmx512M",
-    "-Xms128m",
-    "-Djava.awt.headless=true",
-    "-Dwrapper.logfile=/tmp/router.log",
-    "-Dwrapper.logfile.loglevel=DEBUG",
-    "-Dwrapper.java.pidfile=/tmp/routerjvm.pid",
-    "-Dwrapper.console.loglevel=DEBUG"
+  let defaultStartupCommand:String = "/usr/libexec/java_home"
+  
+  let defaultJavaHomeArgs:[String] = [
+    "-v",
+    "1.7+",
+    "--exec",
+    "java",
   ]
   
-  private func subInit(cmdPath: String?, cmdArgs: String?) {
-    // Use this as common init
-    self.subprocessPath = cmdPath
-    self.arguments = cmdArgs
-    if (self.arguments?.isEmpty)! {
-      self.arguments = Optional.some(defaultStartupFlags.joined(separator: " "))
-    };
-    let newArgs:[String] = ["-c ",
-                            self.subprocessPath!,
-      " ",
-      self.arguments!,
+  let appSupportPath = FileManager.default.urls(for: FileManager.SearchPathDirectory.applicationSupportDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)
+  
+  func SetupAgent() {
+    let agent = SetupAndReturnAgent()
+    RouterRunner.launchAgent = agent
+  }
+  
+  typealias Async = (_ success: () -> Void, _ failure: (NSError) -> Void) -> Void
+  
+  func retry(numberOfTimes: Int, _ sleepForS: UInt32, task: () -> Async, success: () -> Void, failure: (NSError) -> Void) {
+    task()(success, { error in
+      if numberOfTimes > 1 {
+        sleep(sleepForS)
+        retry(numberOfTimes: numberOfTimes - 1, sleepForS, task: task, success: success, failure: failure)
+      } else {
+        failure(error)
+      }
+    })
+  }
+  
+  func SetupAndReturnAgent() -> LaunchAgent {
+    
+    let defaultStartupFlags:[String] = [
+      "-Xmx512M",
+      "-Xms128m",
+      "-Djava.awt.headless=true",
+      "".appendingFormat("-Di2p.base.dir=%@", NSHomeDirectory()+"/Library/I2P"),
+      "".appendingFormat("-Dwrapper.logfile=%@/Library/I2P/router.log", NSHomeDirectory()),
+      "-Dwrapper.logfile.loglevel=DEBUG",
+      "".appendingFormat("-Dwrapper.java.pidfile=%@/i2p/router.pid", appSupportPath.description),
+      "-Dwrapper.console.loglevel=DEBUG",
+      "net.i2p.router.Router"
+    ]
+    
+    self.daemonPath = self.defaultStartupCommand
+    self.arguments = defaultStartupFlags.joined(separator: " ")
+    
+    let basePath = NSHomeDirectory()+"/Library/I2P"
+    
+    let jars = try! FileManager.default.contentsOfDirectory(atPath: basePath+"/lib")
+    var classpath:String = "."
+    for jar in jars {
+      classpath += ":"+basePath+"/lib/"+jar
+    }
+    
+    var cliArgs:[String] = [
+      self.daemonPath!,
+      ]
+    cliArgs.append(contentsOf: self.defaultJavaHomeArgs)
+    cliArgs.append(contentsOf: [
+      "-cp",
+      classpath,
+      ])
+    cliArgs.append(contentsOf: defaultStartupFlags)
+    let agent = LaunchAgent(label: self.domainLabel,program: cliArgs)
+    agent.launchOnlyOnce = false
+    agent.keepAlive = false
+    agent.workingDirectory = basePath
+    agent.userName = NSUserName()
+    agent.standardErrorPath = NSHomeDirectory()+"/Library/Logs/I2P/router.stderr.log"
+    agent.standardOutPath = NSHomeDirectory()+"/Library/Logs/I2P/router.stdout.log"
+    agent.environmentVariables = [
+      "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
+      "I2PBASE": basePath,
     ]
-    self.currentRunningProcess = Optional.some(Subprocess.init(executablePath: "/bin/sh", arguments: newArgs))
+    agent.disabled = false
+    agent.processType = ProcessType.adaptive
+    RouterRunner.launchAgent = agent
+    
+    let userPreferences = UserDefaults.standard
+    let shouldStartupAtLogin = userPreferences.bool(forKey: "startRouterAtLogin")
+    agent.runAtLoad = shouldStartupAtLogin
+    agent.keepAlive = true
+    
+    do {
+      
+      try LaunchAgentManager.shared.write(agent, called: self.plistName)
+      sleep(1)
+      try LaunchAgentManager.shared.load(agent)
+      sleep(1)
+      
+      let agentStatus = LaunchAgentManager.shared.status(agent)
+      switch agentStatus {
+      case .running:
+        break
+      case .loaded:
+        break
+      case .unloaded:
+        sleep(2)
+        break
+      }
+      
+      
+      RouterManager.shared().eventManager.trigger(eventName: "router_can_start", information: agent)
+    } catch {
+      RouterManager.shared().eventManager.trigger(eventName: "router_setup_error", information: "\(error)")
+    }
+    return agent
   }
   
-  init(cmdPath: String?, _ cmdArgs: String? = Optional.none) {
-    super.init()
-    self.subInit(cmdPath: cmdPath, cmdArgs: cmdArgs)
+  func StartAgent(information:Any?) {
+    let agent = RouterRunner.launchAgent!
+    LaunchAgentManager.shared.start(agent)
+    sleep(1)
+    let agentStatus = agent.status()
+    switch agentStatus {
+    case .running(let pid):
+      RouterManager.shared().eventManager.trigger(eventName: "router_start", information: String(pid))
+      routerStatus.setRouterStatus(true)
+      routerStatus.setRouterRanByUs(true)
+      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+        // Delayed message to ensure UI has been initialized.
+        RouterManager.shared().eventManager.trigger(eventName: "router_pid", information: String(pid))
+      }
+      break
+      
+    default: break
+    }
   }
   
-  init(coder: NSCoder) {
-    super.init()
-    self.subInit(cmdPath: Optional.none, cmdArgs: Optional.none)
+  func StopAgent() {
+    var agentStatus = LaunchAgentManager.shared.status(RouterRunner.launchAgent!)
+    switch agentStatus {
+    case .running:
+      LaunchAgentManager.shared.stop(RouterRunner.launchAgent!)
+      break
+    case .loaded, .unloaded:
+      try! LaunchAgentManager.shared.load(RouterRunner.launchAgent!)
+      routerStatus.setRouterStatus(false)
+      routerStatus.setRouterRanByUs(false)
+      RouterManager.shared().eventManager.trigger(eventName: "router_stop", information: "ok")
+      return;
+      break
+    default: break
+    }
+    sleep(1)
+    agentStatus = LaunchAgentManager.shared.status(RouterRunner.launchAgent!)
+    switch agentStatus {
+    case .loaded, .unloaded:
+      try! LaunchAgentManager.shared.load(RouterRunner.launchAgent!)
+      routerStatus.setRouterStatus(false)
+      routerStatus.setRouterRanByUs(false)
+      RouterManager.shared().eventManager.trigger(eventName: "router_stop", information: "ok")
+      break
+    default: break
+    }
   }
   
-  func execute() {
-    if (self.currentRunningProcess != Optional.none!) {
-      print("Already executing! Process ", self.toString())
+  func SetupLaunchd() {
+    do {
+      try LaunchAgentManager.shared.write(RouterRunner.launchAgent!, called: self.plistName)
+      try LaunchAgentManager.shared.load(RouterRunner.launchAgent!)
+    } catch {
+      RouterManager.shared().eventManager.trigger(eventName: "router_exception", information: error)
     }
-    self.timeWhenStarted = Date()
-    RouterProcessStatus.routerStartedAt = self.timeWhenStarted
-    
-    self.currentProcessResults = self.currentRunningProcess?.execute(captureOutput: true)
+  }
+  
+  func TeardownLaunchd() {
+    /*let status = LaunchAgentManager.shared.status(RouterRunner.launchAgent!)
+    switch status {
+    case .running:*/
+      do {
+        // Unload no matter previous state!
+        try LaunchAgentManager.shared.unload(RouterRunner.launchAgent!)
+        
+        let plistPath = NSHomeDirectory()+"/Library/LaunchAgents/"+self.plistName
+        
+        sleep(1)
+        if FileManager.default.fileExists(atPath: plistPath) {
+          try FileManager.default.removeItem(atPath: plistPath)
+        }
+      } catch LaunchAgentManagerError.urlNotSet(label: self.domainLabel) {
+        Logger.MLog(level:3, "URL not set in launch agent")
+      } catch {
+        Logger.MLog(level:3, "".appendingFormat("Error in launch agent: %s", error as CVarArg))
+        RouterManager.shared().eventManager.trigger(eventName: "router_exception", information: error)
+      }
+   /*   break
+    default: break
+    }
+    */
   }
   
 }
-- 
GitLab