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
This commit is contained in:
meeh
2018-09-18 15:36:38 +00:00
parent 1bddf5527a
commit 7615b9236b
47 changed files with 1735 additions and 41 deletions

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu id="TTS-U7-WkT" userLabel="statusBarContextMenu">
<items>
<menuItem title="Open Console" id="cOe-UL-1TW">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="openConsoleClicked:" target="-1" id="uR3-lb-ikG"/>
</connections>
</menuItem>
<menuItem title="Start I2P Router" id="RQ8-Q2-68A">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startRouterClicked:" target="-1" id="Vl3-cC-77e"/>
</connections>
</menuItem>
<menuItem title="Exit" id="hsL-CH-m3C">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="quickClicked:" target="-1" id="hiW-5d-nBX"/>
</connections>
</menuItem>
</items>
<point key="canvasLocation" x="17" y="167"/>
</menu>
</objects>
</document>

View File

@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "SBridge.h"
#import "AppleStuffExceptionHandler.h"

View File

@ -3,47 +3,53 @@
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>I2PLauncher</string>
<key>NSHumanReadableCopyright</key>
<string>Public Domain</string>
<key>CFBundleGetInfoString</key>
<string>0.9.35-experimental</string>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string>images/AppIcon.icns</string>
<string></string>
<key>CFBundleIdentifier</key>
<string>net.i2p.launcher</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>I2P</string>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleSignature</key>
<string>I2P</string>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLIconFile</key>
<string>ItoopieTransparent</string>
<key>CFBundleURLName</key>
<string>http+i2p</string>
<key>CFBundleURLSchemes</key>
<array>
<string>http+i2p</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>0.0.1</string>
<key>NSUserNotificationAlertStyle</key>
<string>alert</string>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<true/>
<key>NSAppleScriptEnabled</key>
<true/>
<key>CGDisableCoalescedUpdates</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.5</string>
<key>CFBundleDisplayName</key>
<string>I2P</string>
<key>LSMinimumSystemVersionByArchitecture</key>
<dict>
<key>i386</key>
<string>10.5.0</string>
<key>x86_64</key>
<string>10.6.0</string>
</dict>
<key>LSUIElement</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 The I2P Project. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>UserInterfaces</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSServices</key>
<array>
<dict/>
</array>
</dict>
</plist>

View File

@ -1,11 +0,0 @@
# The Objective-C++ part of the Mac OS X launcher
## Why?
Code signing, OS X integration.
## Howto build?
Preferred tool is [ninja](https://ninja-build.org/). A makefile is also available.
Build with: `ninja`

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<capability name="box content view" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--PopoverController-->
<scene sceneID="4eF-i8-6L0">
<objects>
<viewController identifier="PopoverView" storyboardIdentifier="PopoverView" id="Ii7-Rb-Ls8" userLabel="PopoverController" customClass="PopoverViewController" customModule="I2PLauncher" customModuleProvider="target" sceneMemberID="viewController">
<tabView key="view" allowsTruncatedLabels="NO" initialItem="Fle-u6-lgB" id="dmf-Go-6qY">
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Router Status &amp; Control" identifier="" id="Fle-u6-lgB">
<view key="view" canDrawConcurrently="YES" id="xXg-Bt-RWR" customClass="RouterStatusView" customModule="I2PLauncher" customModuleProvider="target">
<rect key="frame" x="10" y="33" width="430" height="254"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<box fixedFrame="YES" title="Router information and status" translatesAutoresizingMaskIntoConstraints="NO" id="e8C-qI-SCp">
<rect key="frame" x="6" y="16" width="279" height="231"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" id="zBB-wE-VXr">
<rect key="frame" x="2" y="2" width="275" height="214"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField identifier="RouterStatusLabel" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Eo-re-5WK" userLabel="RouterStatusLabel">
<rect key="frame" x="6" y="190" width="245" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Router status: Unknown" id="m7V-Se-tnf">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField identifier="RouterVersionLabel" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="sGy-NV-gmH" userLabel="RouterVersionLabel">
<rect key="frame" x="6" y="171" width="245" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Router version: Unknown" id="Mda-Os-8O9">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField identifier="RouterStartedByLabel" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tgG-IT-Ojg" userLabel="RouterStartedByLabel">
<rect key="frame" x="6" y="152" width="245" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Router started by launcher? No" id="WBg-nH-kwu">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField identifier="RouterUptimeLabel" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="f2W-Kc-cxB" userLabel="RouterUptimeLabel">
<rect key="frame" x="6" y="133" width="245" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Router uptime: Unknown" id="uQ0-cW-tYL">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
</box>
<box fixedFrame="YES" title="Quick Control" translatesAutoresizingMaskIntoConstraints="NO" id="IIP-Qi-7dp">
<rect key="frame" x="287" y="16" width="150" height="231"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" identifier="QuickControlView" id="D8V-d8-0wT">
<rect key="frame" x="2" y="2" width="146" height="214"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button identifier="startstopRouterButton" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1eu-Qw-TD9" userLabel="start-stop-button">
<rect key="frame" x="0.0" y="176" width="147" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Start/Stop Router" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="OYN-sS-eQH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button identifier="restartRouterButton" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="C1m-1t-Xiq" userLabel="restart-button">
<rect key="frame" x="10" y="150" width="128" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Restart Router" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sih-uF-V06">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button identifier="openConsoleButton" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XZi-oz-5gy" userLabel="open-console-button">
<rect key="frame" x="11" y="123" width="126" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Open Console" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Yh8-lj-JFi">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews>
</view>
</box>
</subviews>
<connections>
<outlet property="quickControlView" destination="D8V-d8-0wT" id="4to-rN-2eL"/>
<outlet property="routerStartStopButton" destination="1eu-Qw-TD9" id="FLt-MK-y5u"/>
<outlet property="routerStartedByLabel" destination="tgG-IT-Ojg" id="dA0-3w-cuF"/>
<outlet property="routerStatusLabel" destination="0Eo-re-5WK" id="7dm-Et-eSn"/>
<outlet property="routerUptimeLabel" destination="f2W-Kc-cxB" id="4Ya-qU-eb3"/>
<outlet property="routerVersionLabel" destination="sGy-NV-gmH" id="tM5-4M-DKy"/>
</connections>
</view>
</tabViewItem>
<tabViewItem label="Router Log Viewer" identifier="" id="IFq-CR-cjD" customClass="LogViewerViewController" customModule="I2PLauncher" customModuleProvider="target">
<view key="view" id="7U9-d7-IVr">
<rect key="frame" x="10" y="33" width="430" height="254"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView identifier="LoggerTextScrollView" fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jca-Kn-cO2" userLabel="LoggerTextScrollView">
<rect key="frame" x="11" y="17" width="409" height="228"/>
<autoresizingMask key="autoresizingMask"/>
<clipView key="contentView" ambiguous="YES" id="E5l-WA-qOn">
<rect key="frame" x="1" y="1" width="407" height="226"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView identifier="LoggerTextView" ambiguous="YES" importsGraphics="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" continuousSpellChecking="YES" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" quoteSubstitution="YES" dashSubstitution="YES" spellingCorrection="YES" smartInsertDelete="YES" id="bgQ-8i-Xgb" userLabel="LoggerTextView">
<rect key="frame" x="0.0" y="0.0" width="407" height="226"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="minSize" width="407" height="226"/>
<size key="maxSize" width="463" height="10000000"/>
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</textView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="MRF-Wt-zdZ">
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="Xq6-ur-WuT">
<rect key="frame" x="392" y="1" width="16" height="226"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
</view>
<connections>
<outlet property="scrollView" destination="jca-Kn-cO2" id="qAi-hi-PsC"/>
<outlet property="textFieldView" destination="bgQ-8i-Xgb" id="SbC-0r-xPR"/>
</connections>
</tabViewItem>
</tabViewItems>
</tabView>
</viewController>
<customObject id="d8g-wS-Zts" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-823" y="166"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,104 @@
//
// SwiftMainDelegate.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
import Cocoa
@objc class SwiftMainDelegate : NSObject {
//let statusItem = NSStatusBar.system().statusItem(withLength: NSSquareStatusItemLength )
let statusBarController = StatusBarController()
let javaDetector = DetectJava()
static let objCBridge = SBridge()
override init() {
super.init()
self.javaDetector.findIt()
if (!javaDetector.isJavaFound()) {
print("Could not find java....")
terminate("No java..")
}
let javaBinPath = self.javaDetector.javaHome
print("Found java home = ", javaBinPath)
let (portIsNotTaken, descPort) = RouterProcessStatus.checkTcpPortForListen(port: 7657)
if (!portIsNotTaken) {
RouterProcessStatus.isRouterRunning = true
RouterProcessStatus.isRouterChildProcess = false
print("I2P Router seems to be running")
} else {
RouterProcessStatus.isRouterRunning = false
print("I2P Router seems to NOT be running")
}
}
func findInstalledI2PVersion(jarPath: String, javaBin: String) {
var i2pPath = NSHomeDirectory()
i2pPath += "/Library/I2P"
var jExecPath:String = javaBin
// Sometimes, home will return the binary, sometimes the actual home dir. This fixes the diverge.
// If JRE is detected, binary follows - if it's JDK, home follows.
if (jExecPath.hasSuffix("Home")) {
jExecPath += "/jre/bin/java"
}
let subCmd = jExecPath + " -cp " + jarPath + " net.i2p.CoreVersion"
var cmdArgs:[String] = ["-c", subCmd]
print(cmdArgs)
let sub:Subprocess = Subprocess.init(executablePath: "/bin/sh", arguments: cmdArgs)
let results:ExecutionResult = sub.execute(captureOutput: true)!
if (results.didCaptureOutput) {
print("captured output")
let i2pVersion = results.outputLines.first?.replace(target: "I2P Core version: ", withString: "")
print("I2P version detected: ",i2pVersion!)
RouterProcessStatus.routerVersion = i2pVersion
} else {
print("did NOT captured output")
}
}
@objc func applicationDidFinishLaunching() {
print("Hello from swift!")
var i2pPath = NSHomeDirectory()
i2pPath += "/Library/I2P"
let javaBinPath = self.javaDetector.javaHome.replace(target: " ", withString: "\\ ")
let fileManager = FileManager()
var ok = ObjCBool(true)
let doesI2PDirExists = fileManager.fileExists(atPath: i2pPath, isDirectory: &ok)
if (!doesI2PDirExists) {
// Deploy
}
let i2pJarPath = i2pPath + "/lib/i2p.jar"
findInstalledI2PVersion(jarPath: i2pJarPath, javaBin: javaBinPath)
}
@objc static func openLink(url: String) {
objCBridge.openUrl(url)
}
@objc func applicationWillTerminate() {
// Shutdown stuff
}
@objc func terminate(_ why: Any?) {
print("Stopping cause of ", why!)
}
}

View File

@ -0,0 +1,29 @@
//
// ArrayExtension.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
extension Array where Element: NSAttributedString {
func joined2(separator: NSAttributedString) -> NSAttributedString {
var isFirst = true
return self.reduce(NSMutableAttributedString()) {
(r, e) in
if isFirst {
isFirst = false
} else {
r.append(separator)
}
r.append(e)
return r
}
}
}

View File

@ -0,0 +1,87 @@
//
// DateTimeUtils.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
extension Date {
init(date: NSDate) {
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
}
}
extension NSDate {
convenience init(date: Date) {
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
}
}
class DateTimeUtils {
static func timeAgoSinceDate(date:NSDate, numericDates:Bool) -> String {
let calendar = NSCalendar.current
let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfYear, .month, .year, .second]
let now = NSDate()
let earliest = now.earlierDate(date as Date)
let latest = (earliest == now as Date) ? date : now
let components = calendar.dateComponents(unitFlags, from: earliest as Date, to: latest as Date)
if (components.year! >= 2) {
return "\(components.year!) years ago"
} else if (components.year! >= 1){
if (numericDates){
return "1 year ago"
} else {
return "Last year"
}
} else if (components.month! >= 2) {
return "\(components.month!) months ago"
} else if (components.month! >= 1){
if (numericDates){
return "1 month ago"
} else {
return "Last month"
}
} else if (components.weekOfYear! >= 2) {
return "\(components.weekOfYear!) weeks ago"
} else if (components.weekOfYear! >= 1){
if (numericDates){
return "1 week ago"
} else {
return "Last week"
}
} else if (components.day! >= 2) {
return "\(components.day!) days ago"
} else if (components.day! >= 1){
if (numericDates){
return "1 day ago"
} else {
return "Yesterday"
}
} else if (components.hour! >= 2) {
return "\(components.hour!) hours ago"
} else if (components.hour! >= 1){
if (numericDates){
return "1 hour ago"
} else {
return "An hour ago"
}
} else if (components.minute! >= 2) {
return "\(components.minute!) minutes ago"
} else if (components.minute! >= 1){
if (numericDates){
return "1 minute ago"
} else {
return "A minute ago"
}
} else if (components.second! >= 3) {
return "\(components.second!) seconds ago"
} else {
return "Just now"
}
}
}

View File

@ -0,0 +1,37 @@
//
// EventMonitor.swift
// I2PLauncher
//
// Created by Mikal Villa on 02/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
import Cocoa
public class EventMonitor {
private var monitor: Any?
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent?) -> Void
public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
public func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
}
public func stop() {
if monitor != nil {
NSEvent.removeMonitor(monitor!)
monitor = nil
}
}
}

View File

@ -0,0 +1,57 @@
//
// ReflectionFunctions.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
class ReflectionFunctions {
/// Given pointer to first element of a C array, invoke a function for each element
func enumerateCArray<T>(array: UnsafePointer<T>, count: UInt32, f: (UInt32, T) -> ()) {
var ptr = array
for i in 0..<count {
f(i, ptr.pointee)
ptr = ptr.successor()
}
}
/// Return name for a method
func methodName(m: Method) -> String? {
let sel = method_getName(m)
let nameCString = sel_getName(sel)
return String(cString: nameCString!)
}
/// Print the names for each method in a class
func printMethodNamesForClass(cls: AnyClass) {
var methodCount: UInt32 = 0
let methodList = class_copyMethodList(cls, &methodCount)
if methodList != nil && methodCount > 0 {
enumerateCArray(array: methodList!, count: methodCount) { i, m in
let name = methodName(m: m!) ?? "unknown"
print("#\(i): \(name)")
}
free(methodList)
}
}
/// Print the names for each method in a class with a specified name
func printMethodNamesForClassNamed(classname: String) {
// NSClassFromString() is declared to return AnyClass!, but should be AnyClass?
let maybeClass: AnyClass? = NSClassFromString(classname)
if let cls: AnyClass = maybeClass {
printMethodNamesForClass(cls: cls)
}
else {
print("\(classname): no such class")
}
}
}

View File

@ -0,0 +1,35 @@
//
// StringExtensions.swift
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
extension String {
func replace(target: String, withString: String) -> String
{
return self.replacingOccurrences(of: target, with: withString, options: NSString.CompareOptions.literal, range: nil)
}
/// Returns an array of string obtained splitting self at each newline ("\n").
/// If the last character is a newline, it will be ignored (no empty string
/// will be appended at the end of the array)
public func splitByNewline() -> [String] {
return self.split { $0 == "\n" }.map(String.init)
}
/// Returns an array of string obtained splitting self at each space, newline or TAB character
public func splitByWhitespace() -> [String] {
let whitespaces = Set<Character>([" ", "\n", "\t"])
return self.split { whitespaces.contains($0) }.map(String.init)
}
public func splitByColon() -> [String] {
return self.split { $0 == ":" }.map(String.init)
}
}

View File

@ -0,0 +1,136 @@
//
// DetectJava.swift
// JavaI2PWrapper
//
// Created by Mikal Villa on 24/03/2018.
// Copyright © 2018 I2P. All rights reserved.
//
import Foundation
class DetectJava : NSObject {
var hasJRE : Bool = false
var userWantJRE : Bool = false
var userAcceptOracleEULA : Bool = false
// Java checks
var javaHome: String = ""{
//Called before the change
willSet(newValue){
print("DetectJava.javaHome will change from "+self.javaHome+" to "+newValue)
}
//Called after the change
didSet{
hasJRE = true
print("MDetectJava.javaHome did change from "+oldValue+" to "+self.javaHome)
}
};
private var testedEnv : Bool = false
private var testedJH : Bool = false
private var testedDfl : Bool = false
func isJavaFound() -> Bool {
if !(self.javaHome.isEmpty) {
return true
}
return false
}
func findIt() {
print("Start with checking environment variable")
self.checkJavaEnvironmentVariable()
if !(self.javaHome.isEmpty) {
RouterProcessStatus.knownJavaBinPath = Optional.some(self.javaHome)
hasJRE = true
return
}
print("Checking default JRE install path")
self.checkDefaultJREPath()
if !(self.javaHome.isEmpty) {
RouterProcessStatus.knownJavaBinPath = Optional.some(self.javaHome)
hasJRE = true
return
}
print("Checking with the java_home util")
self.runJavaHomeCmd()
if !(self.javaHome.isEmpty) {
RouterProcessStatus.knownJavaBinPath = Optional.some(self.javaHome)
hasJRE = true
return
}
}
func runJavaHomeCmd() {
let task = Process()
task.launchPath = "/usr/libexec/java_home"
task.arguments = []
let pipe = Pipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
if (str != nil) {
let stringVal = str! as String
print("got output: "+stringVal)
self.javaHome = stringVal
}
// TODO: Found something, check it out
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NotificationCenter.default.removeObserver(obs1)
// No JRE so far
}
}
var obs2 : NSObjectProtocol!
obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NotificationCenter.default.removeObserver(obs2)
}
task.launch()
task.waitUntilExit()
self.testedJH = true
}
func checkDefaultJREPath() {
let defaultJREPath = "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"
if FileManager.default.fileExists(atPath: defaultJREPath) {
// Found it!!
self.javaHome = defaultJREPath
}
self.testedDfl = true
// No JRE here
}
func getEnvironmentVar(_ name: String) -> String? {
guard let rawValue = getenv(name) else { return nil }
return String(utf8String: rawValue)
}
func checkJavaEnvironmentVariable() {
let dic = ProcessInfo.processInfo.environment
//ProcessInfo.processInfo.environment["JAVA_HOME"]
if let javaHomeEnv = dic["JAVA_HOME"] {
// Maybe we got an JRE
print("Found JAVA_HOME with value:")
print(javaHomeEnv)
self.javaHome = javaHomeEnv
}
self.testedEnv = true
print("JAVA HOME is not set")
}
}

View File

@ -0,0 +1,32 @@
//
// I2PSubprocess.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
protocol I2PSubprocess {
var subprocessPath: String? { get set }
var arguments: String? { get set }
var timeWhenStarted: Date? { get set }
func findJava();
}
extension I2PSubprocess {
func toString() -> String {
let jp = self.subprocessPath!
let args = self.arguments!
var presentation:String = "I2PSubprocess[ cmd="
presentation += jp
presentation += " , args="
presentation += args
presentation += "]"
return presentation
}
}

View File

@ -0,0 +1,28 @@
//
// RouterDeployer.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
class RouterDeployer: NSObject, I2PSubprocess {
var subprocessPath: String?
var timeWhenStarted: Date?
var arguments: String?
func findJava() {
//
}
let javaBinaryPath = RouterProcessStatus.knownJavaBinPath
let defaultFlagsForExtractorJob:[String] = [
"-Xmx512M",
"-Xms128m",
"-Djava.awt.headless=true"
]
}

View File

@ -0,0 +1,18 @@
//
// RouterProcessStatus+ObjectiveC.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
extension RouterProcessStatus {
static func createNewRouterProcess(i2pPath: String, javaBinPath: String) {
let bridge = SBridge()
let timeWhenStarted = Date()
RouterProcessStatus.routerStartedAt = timeWhenStarted
bridge.startupI2PRouter(i2pPath, javaBinPath: javaBinPath)
}
}

View File

@ -0,0 +1,89 @@
//
// RouterProcessStatus.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
import AppKit
class RouterProcessStatus : NSObject {
/**
*
* Why the functions bellow? Because the Objective-C bridge is limited, it can't do Swift "static's" over it.
*
**/
func setRouterStatus(_ isRunning: Bool = false) {
RouterProcessStatus.isRouterRunning = isRunning
}
func setRouterRanByUs(_ ranByUs: Bool = false) {
RouterProcessStatus.isRouterChildProcess = ranByUs
}
func getRouterIsRunning() -> Bool? {
return RouterProcessStatus.isRouterRunning
}
}
extension RouterProcessStatus {
static var isRouterRunning : Bool = false
static var isRouterChildProcess : Bool = false
static var routerVersion : String? = Optional.none
static var routerUptime : 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
}
extension RouterProcessStatus {
static func checkTcpPortForListen(port: in_port_t) -> (Bool, descr: String){
let socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0)
if socketFileDescriptor == -1 {
return (false, "SocketCreationFailed, \(descriptionOfLastError())")
}
var addr = sockaddr_in()
let sizeOfSockkAddr = MemoryLayout<sockaddr_in>.size
addr.sin_len = __uint8_t(sizeOfSockkAddr)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = Int(OSHostByteOrder()) == OSLittleEndian ? _OSSwapInt16(port) : port
addr.sin_addr = in_addr(s_addr: inet_addr("0.0.0.0"))
addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0)
var bind_addr = sockaddr()
memcpy(&bind_addr, &addr, Int(sizeOfSockkAddr))
if Darwin.bind(socketFileDescriptor, &bind_addr, socklen_t(sizeOfSockkAddr)) == -1 {
let details = descriptionOfLastError()
release(socket: socketFileDescriptor)
return (false, "\(port), BindFailed, \(details)")
}
if listen(socketFileDescriptor, SOMAXCONN ) == -1 {
let details = descriptionOfLastError()
release(socket: socketFileDescriptor)
return (false, "\(port), ListenFailed, \(details)")
}
release(socket: socketFileDescriptor)
return (true, "\(port) is free for use")
}
static func release(socket: Int32) {
Darwin.shutdown(socket, SHUT_RDWR)
close(socket)
}
static func descriptionOfLastError() -> String {
return String(cString: UnsafePointer(strerror(errno))) ?? "Error: \(errno)"
}
}

View File

@ -0,0 +1,69 @@
//
// RouterRunner.swift
// I2PLauncher
//
// Created by Mikal Villa on 18/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
import Foundation
class RouterRunner: NSObject, I2PSubprocess {
var subprocessPath: String?
var arguments: String?
var timeWhenStarted: Date?
var currentRunningProcess: Subprocess?
var currentProcessResults: ExecutionResult?
func findJava() {
self.subprocessPath = RouterProcessStatus.knownJavaBinPath
}
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"
]
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: " "))
};
var newArgs:[String] = ["-c ",
self.subprocessPath!,
" ",
self.arguments!,
]
self.currentRunningProcess = Optional.some(Subprocess.init(executablePath: "/bin/sh", arguments: newArgs))
}
init(cmdPath: String?, _ cmdArgs: String? = Optional.none) {
super.init()
self.subInit(cmdPath: cmdPath, cmdArgs: cmdArgs)
}
init(coder: NSCoder) {
super.init()
self.subInit(cmdPath: Optional.none, cmdArgs: Optional.none)
}
func execute() {
if (self.currentRunningProcess != Optional.none!) {
print("Already executing! Process ", self.toString())
}
self.timeWhenStarted = Date()
RouterProcessStatus.routerStartedAt = self.timeWhenStarted
self.currentProcessResults = self.currentRunningProcess?.execute(captureOutput: true)
}
}

View File

@ -0,0 +1,12 @@
//
// AppleStuffExceptionHandler.h
// I2PLauncher
//
// Created by Mikal Villa on 17/09/2018.
// Copyright © 2018 The I2P Project. All rights reserved.
//
#import <Foundation/Foundation.h>
extern NSException* __nullable AppleStuffExecuteWithPossibleExceptionInBlock(dispatch_block_t _Nonnull block);

View File

@ -0,0 +1,22 @@
//
// 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;
}

View File

@ -0,0 +1,22 @@
//
// 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)
}
}

View File

@ -0,0 +1,63 @@
//
// 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
}
}

View File

@ -0,0 +1,118 @@
//
// 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)")
}
}
}

View File

@ -0,0 +1,159 @@
//
// 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 : "" )
}
}

View File

@ -0,0 +1,113 @@
//
// 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
}
}

View File

@ -0,0 +1,24 @@
//
// 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 {
}

View File

@ -0,0 +1,118 @@
//
// 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
}
}

View File

@ -0,0 +1,120 @@
//
// 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)
}
}

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB