I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects
Commit 7615b923 authored by meeh's avatar meeh
Browse files

Adding all new code, removed a lot obsolete code and fixed import paths etc.

Mac OS X launcher:
* UI built on Swift
  * Why?
    * Apple seems to on purpose make it harder to get into Objective-C these days
    * Swift is compiled to native code, but has easiness of Javascript in programming
    * Perfect for the OS X UI, many guides & tutorials as well
* "Backend" in Objective-C++ / C++14
  * Why?
    * Originally written in Objective-C / C++14 with C++17 backports
    * Only for backend because of the time the development takes
    *

Short summary of features:

* Java
  * It can detect java from:
    * JAVA_HOME environment variable
    * "Internet Plug-Ins" Apple stuff
    * By the /usr/libexec/java_home binary helper
  * It can unpack a new version of I2P
    * Unpacks to ~/Library/I2P
    * Can check currently unpacked version in ~/Library/I2P via i2p.jar's "net.i2p.CoreVersion"

  * User Interface (a popover, see https://youtu.be/k8L3lQ5rUq0 for example of this concept)
    * Router control tab view
      * It can start the router
      * It can stop the router
      * It can detect already running router, then avoid fireing up one
      * It can show basic information about the router state & version
    * Log view tab (not yet done)
  * While left-click triggers popover, right-click draws a minimal context menu
parent 1bddf552
No related branches found
No related tags found
No related merge requests found
Showing
with 759 additions and 0 deletions
//
// AppleStuffExceptionHandler.m
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
#import "AppleStuffExceptionHandler.h"
NSException* __nullable AppleStuffExecuteWithPossibleExceptionInBlock(dispatch_block_t _Nonnull block) {
@try {
if (block != nil) {
block();
}
}
@catch (NSException *exception) {
return exception;
}
return nil;
}
//
// Error.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public class Error {
/// Prints to console the arguments and exits with status 1
static func die(arguments: Any...) -> Never {
let output = "ERROR: " + arguments.reduce("") { $0 + "\($1) " }
let trimOutput = output.trimmingCharacters(in: CharacterSet.whitespaces) + "\n"
let stderr = FileHandle.standardError
stderr.write(trimOutput.data(using: String.Encoding.utf8)!)
exit(1)
}
}
//
// ExecutionResult.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public struct ExecutionResult {
/// Whether the output was captured
public let didCaptureOutput : Bool
/// The return status of the last subprocess
public var status: Int32 {
return pipelineStatuses.last!
}
/// Return status of all subprocesses in the pipeline
public let pipelineStatuses : [Int32]
/// The output of the subprocess. Empty string if no output was produced or not captured
public let output: String
/// The error output of the last subprocess. Empty string if no error output was produced or not captured
public var errors : String {
return pipelineErrors?.last ?? ""
}
/// The error output of all subprocesses in the pipeline. Empty string if no error output was produced or not captured
public let pipelineErrors : [String]?
/// The output, split by newline
/// - SeeAlso: `output`
public var outputLines : [String] {
return self.output.splitByNewline()
}
/// The error output, split by newline
/// - SeeAlso: `output`
public var errorsLines : [String] {
return self.errors.splitByNewline()
}
/// An execution result where no output was captured
init(pipelineStatuses: [Int32]) {
self.pipelineStatuses = pipelineStatuses
self.didCaptureOutput = false
self.pipelineErrors = nil
self.output = ""
}
/// An execution result where output was captured
init(pipelineStatuses: [Int32], pipelineErrors : [String], output : String) {
self.pipelineStatuses = pipelineStatuses
self.pipelineErrors = pipelineErrors
self.output = output
self.didCaptureOutput = true
}
}
//
// Subprocess+CompactAPI.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
// MARK: - Compact API
extension Subprocess {
/**
Executes a subprocess and wait for completion, returning the output. If there is an error in creating the task,
it immediately exits the process with status 1
- returns: the output as a String
- note: in case there is any error in executing the process or creating the task, it will halt execution. Use
the constructor and `output` instance method for a more graceful error handling
*/
public static func output(
executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") -> String {
let process = Subprocess.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory)
guard let result = process.execute(captureOutput: true) else {
Error.die(arguments: "Can't execute \"\(process)\"")
}
if result.status != 0 {
let errorLines = result.errors == "" ? "" : "\n" + result.errors
Error.die(arguments: "Process \"\(process)\" returned status \(result.status)", errorLines)
}
return result.output
}
/**
Executes a subprocess and wait for completion, returning the execution result. If there is an error in creating the task,
it immediately exits the process with status 1
- returns: the execution result
- note: in case there is any error in executing the process or creating the task, it will halt execution. Use
the constructor and `execute` instance method for a more graceful error handling
*/
public static func execute(
executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") -> ExecutionResult {
let process = Subprocess.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory)
guard let result = process.execute(captureOutput: true) else {
Error.die(arguments: "Can't execute \"\(process)\"")
}
return result
}
/**
Executes a subprocess and wait for completion, returning the output as an array of lines. If there is an error
in creating or executing the task, it immediately exits the process with status 1
- returns: the output as a String
- note: in case there is any error in executing the process or creating the task, it will halt execution. Use
the constructor and `output` instance method for a more graceful error handling
*/
public static func outputLines(
executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") -> [String] {
let process = Subprocess.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory)
guard let result = process.execute(captureOutput: true) else {
Error.die(arguments: "Can't execute \"\(process)\"")
}
if result.status != 0 {
let errorLines = result.errors == "" ? "" : "\n" + result.errors
Error.die(arguments: "Process \"\(process)\" returned status \(result.status)", errorLines)
}
return result.outputLines
}
/**
Executes a subprocess and wait for completion, returning the exit status. If there is an error in creating the task,
it immediately exits the process with status 1
- returns: the output as a String
- note: in case there is any error in launching the process or creating the task, it will halt execution. Use
the constructor and the `run` instance method for a more graceful error handling
*/
public static func run(
executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") -> Int32 {
let process = Subprocess.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory)
guard let result = process.run() else {
Error.die(arguments: "Can't execute \"\(process)\"")
}
return result
}
/**
Executes a subprocess and wait for completion. If there is an error in creating the task, or if the tasks
returns an exit status other than 0, it immediately exits the process with status 1
- note: in case there is any error in launching the process or creating the task, or if the task exists with a exit status other than 0, it will halt execution. Use
the constructor and `run` instance method for a more graceful error handling
*/
public static func runOrDie(
executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") {
let process = Subprocess.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory)
guard let result = process.run() else {
Error.die(arguments: "Can't execute \"\(process)\"")
}
if result != 0 {
Error.die(arguments: "Process \"\(process)\" returned status \(result)")
}
}
}
//
// Subprocess.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
public class Subprocess {
/// The path to the executable
let executablePath : String
/// Arguments to pass to the executable
let arguments : [String]
/// Working directory for the executable
let workingDirectory : String
/// Process to pipe to, if any
let pipeDestination : Subprocess?
public convenience init(
executablePath: String,
arguments: [String] = [],
workingDirectory: String = "."
) {
self.init(executablePath: executablePath,
arguments: arguments,
workingDirectory: workingDirectory,
pipeTo: nil)
}
public init(
executablePath: String,
arguments: [String] = [],
workingDirectory: String = ".",
pipeTo: Subprocess?
) {
self.executablePath = executablePath
self.arguments = arguments
self.workingDirectory = workingDirectory
self.pipeDestination = pipeTo
}
/**
Returns a subprocess ready to be executed
- SeeAlso: init(executablePath:arguments:workingDirectory)
*/
public convenience init(
_ executablePath: String,
_ arguments: String...,
workingDirectory: String = ".") {
self.init(executablePath: executablePath, arguments: arguments, workingDirectory: workingDirectory, pipeTo: nil)
}
}
// Public API
extension Subprocess {
/**
Executes the subprocess and wait for completition, returning the exit status
- returns: the termination status, or nil if it was not possible to execute the process
*/
public func run() -> Int32? {
return self.execute(captureOutput: false)?.status
}
/**
Executes the subprocess and wait for completion, returning the output
- returns: the output of the process, or nil if it was not possible to execute the process
- warning: the entire output will be stored in a String in memory
*/
public func output() -> String? {
return self.execute(captureOutput: true)?.output
}
/**
Executes the subprocess and wait for completition, returning the exit status
- returns: the execution result, or nil if it was not possible to execute the process
*/
public func execute(captureOutput: Bool = false) -> ExecutionResult? {
return buildPipeline(captureOutput: captureOutput).run()
}
}
// Piping of STDIN, STDERR and STDOUT
extension Subprocess {
/// Pipes the output to this process to another process.
/// Will return a new subprocess, you should execute that subprocess to
/// run the entire pipe
public func pipe(to destination: Subprocess) -> Subprocess {
let downstreamProcess : Subprocess
if let existingPipe = self.pipeDestination {
downstreamProcess = existingPipe.pipe(to: destination)
} else {
downstreamProcess = destination
}
return Subprocess(executablePath: self.executablePath, arguments: self.arguments, workingDirectory: self.workingDirectory, pipeTo: downstreamProcess)
}
}
public func | (lhs: Subprocess, rhs: Subprocess) -> Subprocess {
return lhs.pipe(to: rhs)
}
public func | (lhs: String, rhs: String) -> String {
return "(\(lhs)\(rhs))"
}
// MARK: - Process execution
public enum SubprocessError : LocalizedError {
case Error(status: Int, message: String)
}
extension Subprocess {
/// Returns the task to execute
private func task() -> Process {
let task = Process()
task.launchPath = self.executablePath
task.arguments = self.arguments
task.currentDirectoryPath = self.workingDirectory
return task
}
/// Returns the task pipeline for all the downstream processes
public func buildPipeline(captureOutput: Bool, input: AnyObject? = nil) -> TaskPipeline {
let task = self.task()
if let inPipe = input {
task.standardInput = inPipe
}
if let downstreamProcess = self.pipeDestination {
let downstreamPipeline = downstreamProcess.buildPipeline(captureOutput: captureOutput, input: task.standardOutput as AnyObject)
return downstreamPipeline.addToHead(task: task)
}
return TaskPipeline(task: task, captureOutput: captureOutput)
}
}
// Description for pretty print etc.
extension Subprocess : CustomStringConvertible {
public var description : String {
return self.executablePath
+ (self.arguments.count > 0
? " " + self.arguments
.map { $0.replace(target: "\\ ", withString: " ") }
.joined(separator: " ")
: ""
)
+ (self.pipeDestination != nil ? " | " + self.pipeDestination!.description : "" )
}
}
//
// TaskPipeline.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
// Extend the stdlib with the extension bellow
extension Process {
/// Launches a task, captures any objective-c exception and relaunches it as Swift error
public func launchCapturingExceptions() throws {
if let exception = AppleStuffExecuteWithPossibleExceptionInBlock({
self.launch()
}) {
let reason = exception.reason ?? "unknown error"
throw SubprocessError.Error(status: -1, message: reason)
}
}
}
/// A pipeline of tasks, connected in a cascade pattern with pipes
public struct TaskPipeline {
/// List of tasks in the pipeline
let tasks: [Process]
/// Output pipe
let outputPipe: Pipe?
/// Whether the pipeline should capture output to stdErr and stdOut
let captureOutput : Bool
/// Adds a task to the head of the pipeline, that is, the task will provide the input
/// for the first task currently on the head of the pipeline
func addToHead(task: Process) -> TaskPipeline {
guard let firstTask = tasks.first else {
fatalError("Expecting at least one task")
}
let inoutPipe = Pipe()
firstTask.standardInput = inoutPipe
task.standardOutput = inoutPipe
var errorPipe : Pipe?
if self.captureOutput {
errorPipe = Pipe()
task.standardError = errorPipe
}
return TaskPipeline(tasks: [task] + self.tasks, outputPipe: self.outputPipe, captureOutput: self.captureOutput)
}
/// Start all tasks in the pipeline, then wait for them to complete
/// - returns: the return status of the last process in the pipe, or nil if there was an error
func run() -> ExecutionResult? {
let runTasks = launchAndReturnNotFailedTasks()
if runTasks.count != self.tasks.count {
// dropped a task? it's because it failed to start, so error
return nil
}
runTasks.forEach { $0.waitUntilExit() }
// exit status
let exitStatuses = runTasks.map { $0.terminationStatus }
guard captureOutput else {
return ExecutionResult(pipelineStatuses: exitStatuses)
}
// output
let errorOutput = runTasks.map { task -> String in
guard let errorPipe = task.standardError as? Pipe else { return "" }
let readData = errorPipe.fileHandleForReading.readDataToEndOfFile()
return String(data: readData, encoding: String.Encoding.utf8)!
}
let output = String(data: self.outputPipe!.fileHandleForReading.readDataToEndOfFile(), encoding: String.Encoding.utf8)!
return ExecutionResult(pipelineStatuses: exitStatuses, pipelineErrors: errorOutput, output: output)
}
/// Run all tasks and return the tasks that did not fail to launch
private func launchAndReturnNotFailedTasks() -> [Process] {
return self.tasks.flatMap { task -> Process? in
do {
try task.launchCapturingExceptions()
return task
} catch {
return nil
}
}
}
init(task: Process, captureOutput: Bool) {
self.tasks = [task]
self.captureOutput = captureOutput
if captureOutput {
self.outputPipe = Pipe()
task.standardOutput = self.outputPipe
let errorPipe = Pipe()
task.standardError = errorPipe
} else {
self.outputPipe = nil
}
}
private init(tasks: [Process], outputPipe: Pipe?, captureOutput: Bool) {
self.tasks = tasks
self.outputPipe = outputPipe
self.captureOutput = captureOutput
}
}
//
// LogViewController.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
import AppKit
class LogViewerViewController : NSTabViewItem {
@IBOutlet var scrollView: NSScrollView?
@IBOutlet var textFieldView: NSTextView?
}
class LogViewController {
}
//
// PopoverViewController.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Cocoa
class PopoverViewController: NSViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
//super.init(nibName: "UserInterfaces", bundle: Bundle.main)!
//let nib = NSNib(nibNamed: "UserInterfaces", bundle: Bundle.main)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
@objc class RouterStatusView : NSView {
static var instance: RouterStatusView?
static func getInstance() -> RouterStatusView? {
if (self.instance != Optional.none) {
return RouterStatusView.instance
}
return Optional.none
}
@IBOutlet var routerStatusLabel: NSTextField?
@IBOutlet var routerVersionLabel: NSTextField?
@IBOutlet var routerStartedByLabel: NSTextField?
@IBOutlet var routerUptimeLabel: NSTextField?
@IBOutlet var quickControlView: NSView?
@IBOutlet var routerStartStopButton: NSButton?
@objc func actionBtnStartRouter(_ sender: Any?) {
NSLog("START ROUTER")
(sender as! NSButton).cell?.stringValue = "Stop Router"
let timeWhenStarted = Date()
RouterProcessStatus.routerStartedAt = timeWhenStarted
SwiftMainDelegate.objCBridge.startupI2PRouter(RouterProcessStatus.i2pDirectoryPath, javaBinPath: RouterProcessStatus.knownJavaBinPath!)
}
@objc func actionBtnStopRouter(_ sender: Any?) {
NSLog("STOP ROUTER")
}
@objc func actionBtnRestartRouter(sender: Any?) {}
override func viewWillDraw() {
super.viewWillDraw()
if (RouterStatusView.instance == Optional.none) {
RouterStatusView.instance = self
}
self.setRouterStatusLabelText()
}
func setRouterStatusLabelText() {
if (RouterProcessStatus.isRouterRunning) {
routerStatusLabel?.cell?.stringValue = "Router status: Running"
routerStartStopButton?.action = #selector(self.actionBtnStopRouter(_:))
} else {
routerStatusLabel?.cell?.stringValue = "Router status: Not running"
routerStartStopButton?.action = #selector(self.actionBtnStartRouter(_:))
}
routerStartStopButton?.needsDisplay = true
routerStartStopButton?.target = self
quickControlView?.needsDisplay = true
if (RouterProcessStatus.routerVersion?.isEmpty)! {
routerVersionLabel?.cell?.stringValue = "Router version: Still unknown"
} else {
routerVersionLabel?.cell?.stringValue = "Router version: " + RouterProcessStatus.routerVersion!
}
if (RouterProcessStatus.routerStartedAt != Optional.none) {
routerUptimeLabel?.cell?.stringValue = "Router has runned for " + DateTimeUtils.timeAgoSinceDate(date: NSDate(date: RouterProcessStatus.routerStartedAt!), numericDates: false)
}
}
init() {
let c = NSCoder()
super.init(coder: c)!
self.setRouterStatusLabelText()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
self.setRouterStatusLabelText()
}
}
extension PopoverViewController {
static func freshController() -> PopoverViewController {
let storyboard = NSStoryboard(name: "Storyboard", bundle: Bundle.main)
//2.
let identifier = NSStoryboard.SceneIdentifier(string: "PopoverView")
//3.
guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier as String) as? PopoverViewController else {
fatalError("Why cant i find PopoverViewController? - Check PopoverViewController.storyboard")
}
//let viewcontroller = PopoverViewController()
return viewcontroller
}
}
//
// StatusBarController.swift
// I2PLauncher
//
// Created by Mikal Villa on 13/03/2018.
// Copyright © 2018 I2P. All rights reserved.
//
import Foundation
import Cocoa
@objc class StatusBarController: NSObject, NSMenuDelegate {
let popover = NSPopover()
let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength)
//let storyboard = NSStoryboard(name: "Storyboard", bundle: nil)
@objc func handleOpenConsole(_ sender: Any?) {
SwiftMainDelegate.openLink(url: "http://localhost:7657")
}
@objc func constructMenu() -> NSMenu {
let menu = NSMenu()
let sb = SwiftMainDelegate.objCBridge
menu.addItem(NSMenuItem(title: "Open I2P Console", action: #selector(self.handleOpenConsole(_:)), keyEquivalent: "O"))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit I2P Launcher", action: #selector(SwiftMainDelegate.terminate(_:)), keyEquivalent: "q"))
return menu
}
override init() {
super.init()//(xib: "UserInterface", bundle: nil)
popover.contentViewController = PopoverViewController.freshController()
if let button = statusItem.button {
button.image = NSImage(named:"StatusBarButtonImage")
//button.title = "I2P"
button.toolTip = "I2P Launch Manager"
//button.isVisible = true
//button.action = #selector(self.statusBarButtonClicked)
//button.sendAction(on: [.leftMouseUp, .rightMouseUp])
//button.doubleAction = #selector(self.systemBarIconDoubleClick)
button.target = self
button.action = #selector(self.statusBarButtonClicked(sender:))
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
}
@IBAction func openConsoleClicked(_ sender: Any) {
NSLog("openConsoleClicked got clicked")
let realSender = sender as! NSMenuItem
NSWorkspace.shared().open(URL(string: "http://127.0.0.1:7657")!)
NSLog("Sender: @%", realSender)
}
@IBAction func quitClicked(_ sender: NSMenuItem) {
NSApplication.shared().terminate(self)
}
// Submenu
@IBAction func startRouterClicked(_ sender: NSMenuItem) {
}
@IBAction func restartRouterClicked(_ sender: NSMenuItem) {
}
@IBAction func stopRouterClicked(_ sender: NSMenuItem) {
}
func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEventType.rightMouseUp {
closePopover(sender: nil)
let ctxMenu = constructMenu()
statusItem.menu = ctxMenu
statusItem.popUpMenu(ctxMenu)
// This is critical, otherwise clicks won't be processed again
statusItem.menu = nil
} else {
togglePopover(sender: nil)
}
}
func togglePopover(sender: AnyObject?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}
func showPopover(sender: AnyObject?) {
if let button = statusItem.button {
let inst = RouterStatusView.getInstance()
if (inst != nil) {
if (inst != Optional.none) { RouterStatusView.getInstance()?.setRouterStatusLabelText() }
}
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
func closePopover(sender: AnyObject?) {
popover.performClose(sender)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment