Mac OS X Launcher - reborn - ALPHA!

TLDR;
Howto? ant osxLauncher
Privacy Notes? If you don't got SBT, a bash script will trigger
               download of SBT for you with task osxLauncher.
Results? open ./launchers/output
"Binary" App Bundle name: I2P.app
Runtime base directory? ~/Library/I2P
Runtime config directory? untouched.

After talk on IRC with zzz, I rewrote the logic since we could
start with a simple deploy, for a faster alpha version ready :)

SBT will build a zip file from the content of pkg-temp, which
CompleteDeployment.scala will again unzip in runtime. Right now
it's quite basic, but the plan is to add version detection, so
it's capable of upgrading a already deployed I2P base directory.

OSXDeployment.scala is renamed to PartialDeployment.scala for usage
in the browser bundle launcher, since it's going to be a subset of
the files found in pkg-temp.

A Info.plist is added to the launchers/macosx which is added to the
application bundle under building. Note that this differ from the one
in Start i2p router.app that's been here for years now.
This commit is contained in:
meeh
2018-05-05 23:34:35 +00:00
parent f6273a1662
commit 5f81a8de59
11 changed files with 327 additions and 63 deletions

View File

@ -59,6 +59,7 @@ launchers/target
launchers/project/target
launchers/common/target
launchers/output
launchers/project/project
# Reporting
sloccount.sc

View File

@ -301,22 +301,22 @@
<target name="bbLauncher" depends="build">
<sequential>
<exec executable="sbt" dir="launcher" failonerror="true">
<exec executable="sbt" dir="launchers" failonerror="true">
<arg value="browserbundle:clean" />
</exec>
<exec executable="sbt" dir="launcher" failonerror="true">
<exec executable="sbt" dir="launchers" failonerror="true">
<arg value="browserbundle:assembly" />
</exec>
</sequential>
</target>
<target name="osxLauncher" depends="build">
<target name="osxLauncher" depends="build,preppkg-osx">
<sequential>
<exec executable="sbt" dir="launcher" failonerror="true">
<arg value="macosx:clean" />
<exec executable="sbt" dir="launchers" failonerror="true">
<arg value="macosx:cleanAllTask" />
</exec>
<exec executable="sbt" dir="launcher" failonerror="true">
<arg value="macosx:assembly" />
<exec executable="sbt" dir="launchers" failonerror="true">
<arg value="macosx:buildAppBundleTask" />
</exec>
</sequential>
</target>
@ -907,7 +907,7 @@
<classpath>
<pathelement location="build/i2p.jar" />
<pathelement location="build/router.jar" />
<pathelement location="${junit.home}/junit4.jar" />
<pathelement location="${junit.home}/ant-junit4.jar" />
<pathelement location="${hamcrest.home}/hamcrest-all.jar" />
</classpath>
<!--

View File

@ -1,3 +1,14 @@
2018-05-06 meeh
* launchers:
- rewritten some logic
- made CompleteDeployment which extracts i2pbase.zip
- made a SBT task that creates i2pbase.zip
- after the hassle I had even getting an JRE7, short: OSX assumes JRE8+
- started on a class SystemTrayManager which also holds router state info for menu item filter
- "ant osxLauncher" produces now a valid app bundle under launchers/output
* Updated ant and made osxLauncher work again (first time triggers SBT dl if not already installed.)
* monotone ignore file update
2018-05-05 zzz
* i2ptunnel:
- Link to SSL wizard

View File

@ -0,0 +1,48 @@
package net.i2p.launchers
import java.io.{File, FileInputStream, FileOutputStream, InputStream}
import java.nio.file.Path
import java.util.zip.ZipInputStream
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
/**
*
* CompleteDeployment - In use to deploy base path for the Mac OS X Bundle release.
*
* @author Meeh
* @since 0.9.35
*/
class CompleteDeployment(val zipFile: File, val i2pBaseDir: File) {
if (!i2pBaseDir.exists()) {
i2pBaseDir.mkdirs()
} else {
// TODO: Check what version etc..
}
def unzip(zipFile: InputStream, destination: Path): Unit = {
val zis = new ZipInputStream(zipFile)
Stream.continually(zis.getNextEntry).takeWhile(_ != null).foreach { file =>
if (!file.isDirectory) {
val outPath = destination.resolve(file.getName)
val outPathParent = outPath.getParent
if (!outPathParent.toFile.exists()) {
outPathParent.toFile.mkdirs()
}
val outFile = outPath.toFile
val out = new FileOutputStream(outFile)
val buffer = new Array[Byte](4096)
Stream.continually(zis.read(buffer)).takeWhile(_ != -1).foreach(out.write(buffer, 0, _))
}
}
}
def makeDeployment : Future[Unit] = Future {
unzip(new FileInputStream(zipFile), i2pBaseDir.toPath)
}
}

View File

@ -12,7 +12,9 @@ import collection.JavaConverters._
/**
*
* OSXDeployment
* NOTE: Work in progress: Originally written for OSX launcher - but will be used in BB launcher.
*
* PartialXDeployment
*
* This class can be a bit new for java developers. In Scala, when inherit other classes,
* you would need to define their arguments if the super class only has constructors taking arguments.
@ -76,7 +78,7 @@ import collection.JavaConverters._
* @author Meeh
* @since 0.9.35
*/
class OSXDeployment extends
class PartialDeployment extends
DeployProfile(
OSXDefaults.getOSXConfigDirectory.getAbsolutePath,
OSXDefaults.getOSXBaseDirectory.getAbsolutePath

View File

@ -2,6 +2,9 @@ package net.i2p.launchers
import java.io.File
import scala.concurrent.Future
import scala.sys.process.Process
/**
* A abstract class is kind of like an java interface.
@ -10,7 +13,7 @@ import java.io.File
* @since 0.9.35
*/
abstract class RouterLauncher {
def runRouter(basePath: File, args: Array[String]): Unit
def runRouter(basePath: File, args: Array[String]): Future[Process]
def runRouter(args: Array[String]): Unit
def runRouter(args: Array[String]): Future[Process]
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>I2P</string>
<key>NSHumanReadableCopyright</key>
<string>Public Domain</string>
<key>CFBundleGetInfoString</key>
<string>0.9.35-experimental</string>
<key>CFBundleIconFile</key>
<string>i2p</string>
<key>CFBundleIdentifier</key>
<string>net.i2p</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>I2P</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleSignature</key>
<string>I2P</string>
<key>CFBundleVersion</key>
<string>0.0.1</string>
<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>
</dict>
</plist>

View File

@ -1,14 +1,12 @@
import sbtassembly.AssemblyPlugin.defaultShellScript
import sbt._
import Keys._
import sbt.io.IO
import java.io.File
import java.io.{File, FileNotFoundException, FileOutputStream}
import java.util.zip._
lazy val i2pVersion = "0.9.34"
lazy val cleanAllTask = taskKey[Unit]("Clean up and remove the OSX bundle")
lazy val buildAppBundleTask = taskKey[Unit](s"Build an Mac OS X bundle for I2P ${i2pVersion}.")
lazy val bundleBuildPath = file("./output")
lazy val buildDeployZipTask = taskKey[String](s"Build an zipfile with base directory for I2P ${i2pVersion}.")
lazy val bundleBuildPath = new File("./output")
lazy val staticFiles = List(
"blocklist.txt",
@ -32,7 +30,7 @@ def defaultOSXLauncherShellScript(javaOpts: Seq[String] = Seq.empty): Seq[String
Seq(
"#!/usr/bin/env sh",
s"""
|echo "Yo"
|echo "I2P - Mac OS X Launcher starting up"
|export I2P=$$HOME/Library/I2P
|for jar in `ls $${I2P}/lib/*.jar`; do
| if [ ! -z $$CP ]; then
@ -66,7 +64,6 @@ assemblyOption in assembly := (assemblyOption in assembly).value.copy(
"-Dwrapper.logfile.loglevel=DEBUG",
"-Dwrapper.java.pidfile=/tmp/routerjvm.pid",
"-Dwrapper.console.loglevel=DEBUG",
"-Djava.awt.headless=true",
"-Di2p.dir.base=$I2P",
"-Djava.library.path=$I2P"
)))
@ -92,36 +89,66 @@ cleanAllTask := {
IO.delete(bundleBuildPath)
}
buildDeployZipTask := {
println(s"Starting the zip file build process. This might take a while..")
if (!bundleBuildPath.exists()) bundleBuildPath.mkdir()
val sourceDir = i2pBuildDir
def recursiveListFiles(f: File): Array[File] = {
val these = f.listFiles
these ++ these.filter { f => f.isDirectory }.flatMap(recursiveListFiles).filter(!_.isDirectory)
}
def zip(out: String, files: Iterable[String]) = {
import java.io.{ BufferedInputStream, FileInputStream, FileOutputStream }
import java.util.zip.{ ZipEntry, ZipOutputStream }
val zip = new ZipOutputStream(new FileOutputStream(out))
files.foreach { name =>
val fname = sourceDir.toURI.relativize(new File(name).toURI).toString
//println(s"Zipping ${fname}")
if (!new File(name).isDirectory) {
zip.putNextEntry(new ZipEntry(fname))
val in = new BufferedInputStream(new FileInputStream(name))
var b = in.read()
while (b > -1) {
zip.write(b)
b = in.read()
}
in.close()
zip.closeEntry()
}
}
zip.close()
}
val fileList = recursiveListFiles(sourceDir.getCanonicalFile).toList
val zipFileName = new File(bundleBuildPath, "i2pbase.zip").getCanonicalPath
zip(zipFileName, fileList.map { f => f.toString }.toIterable)
zipFileName.toString
}
buildAppBundleTask := {
println(s"Building Mac OS X bundle for I2P version ${i2pVersion}.")
bundleBuildPath.mkdir()
if (!bundleBuildPath.exists()) bundleBuildPath.mkdir()
val paths = Map[String,File](
"execBundlePath" -> new File(bundleBuildPath, "I2P.app/Contents/MacOS"),
"resBundlePath" -> new File(bundleBuildPath, "I2P.app/Contents/Resources"),
"i2pbaseBunldePath" -> new File(bundleBuildPath, "I2P.app/Contents/Resources/i2pbase"),
"i2pJarsBunldePath" -> new File(bundleBuildPath, "I2P.app/Contents/Resources/i2pbase/lib"),
"webappsBunldePath" -> new File(bundleBuildPath, "I2P.app/Contents/Resources/i2pbase/webapps")
"resBundlePath" -> new File(bundleBuildPath, "I2P.app/Contents/Resources")
)
paths.map { case (s,p) => p.mkdirs() }
val dirsToCopy = List("certificates","locale","man")
val launcherBinary = Some(assembly.value)
launcherBinary.map { l => IO.copyFile( new File(l.toString), new File(paths.get("execBundlePath").get, "I2P") ) }
/**
*
* First of, if "map" is unknown for you - shame on you :p
*
* It's a loop basically where it loops through a list/array
* with the current indexed item as subject.
*
* The code bellow takes the different lists and
* copy all the directories or files from the i2p.i2p build dir,
* and into the bundle so the launcher will know where to find i2p.
*
*/
dirsToCopy.map { d => IO.copyDirectory( new File(resDir, d), new File(paths.get("i2pbaseBunldePath").get, d) ) }
warsForCopy.map { w => IO.copyFile( new File(new File(i2pBuildDir, "webapps"), w), new File(paths.get("webappsBunldePath").get, w) ) }
jarsForCopy.map { j => IO.copyFile( new File(new File(i2pBuildDir, "lib"), j), new File(paths.get("i2pJarsBunldePath").get, j) ) }
val plistFile = new File("./macosx/Info.plist")
if (plistFile.exists()) {
println(s"Adding Info.plist...")
IO.copyFile(plistFile, new File(bundleBuildPath, "I2P.app/Contents/Info.plist"))
}
val zipFilePath = Some(buildDeployZipTask.value)
val zipFileOrigin = new File(zipFilePath.get)
IO.copyFile(zipFileOrigin, new File(paths.get("resBundlePath").get, "i2pbase.zip"))
println(s"Zip placed into bundle :)")
}

View File

@ -1,8 +1,15 @@
package net.i2p.launchers.osx
import java.awt.SystemTray
import java.io.File
import net.i2p.launchers.{DeployProfile, OSXDefaults, OSXDeployment}
import net.i2p.launchers.{CompleteDeployment, OSXDefaults}
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.sys.process.Process
import scala.util.{Failure, Success}
/**
*
@ -36,14 +43,53 @@ object LauncherAppMain extends App {
val i2pBaseDir = OSXDefaults.getOSXBaseDirectory
new OSXDeployment()
val selfDirPath = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath).getParentFile
// Tricky to get around, but feels hard to use a "var" which means mutable..
// It's like cursing in the church... Worse.
var sysTray: Option[SystemTrayManager] = None
val deployment = new CompleteDeployment(new File(selfDirPath.getPath, "../Resources/i2pbase.zip"), i2pBaseDir)
val depProc = deployment.makeDeployment
// Change directory to base dir
System.setProperty("user.dir", i2pBaseDir.getAbsolutePath)
// System shutdown hook
sys.ShutdownHookThread {
println("exiting launcher process")
}
Await.ready(depProc, 60000 millis)
println("I2P Base Directory Extracted.")
try {
MacOSXRouterLauncher.runRouter(i2pBaseDir, args)
val routerProcess: Future[Process] = MacOSXRouterLauncher.runRouter(i2pBaseDir, args)
if (SystemTray.isSupported) {
sysTray = Some(new SystemTrayManager)
}
routerProcess onComplete {
case Success(forkResult) => {
println(s"Router started successfully!")
try {
val routerPID = MacOSXRouterLauncher.pid(forkResult)
println(s"PID is ${routerPID}")
} catch {
case ex:java.lang.RuntimeException => println(s"Minor error: ${ex.getMessage}")
}
if (!sysTray.isEmpty) sysTray.get.setRunning(true)
}
case Failure(fail) => {
println(s"Router failed to start, error is: ${fail.toString}")
}
}
//Await.result(routerProcess, 5000 millis)
} finally {
System.out.println("Exit.")
System.out.println("Exit?")
}
}

View File

@ -1,10 +1,14 @@
package net.i2p.launchers.osx
import java.io.File
import java.lang.reflect.Field
import scala.sys.process.Process
import net.i2p.launchers.RouterLauncher
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
/**
*
*
@ -13,25 +17,73 @@ import net.i2p.launchers.RouterLauncher
*/
object MacOSXRouterLauncher extends RouterLauncher {
override def runRouter(args: Array[String]): Unit = {}
def pid(p: Process): Long = {
val procField = p.getClass.getDeclaredField("p")
procField.synchronized {
procField.setAccessible(true)
val proc = procField.get(p)
try {
proc match {
case unixProc
if unixProc.getClass.getName == "java.lang.UNIXProcess" => {
val pidField = unixProc.getClass.getDeclaredField("pid")
pidField.synchronized {
pidField.setAccessible(true)
try {
pidField.getLong(unixProc)
} finally {
pidField.setAccessible(false)
}
}
}
case procImpl:java.lang.Process => {
val f: Field = p.getClass().getDeclaredField("p")
val f2: Field = f.get(p).getClass.getDeclaredField("pid")
try {
f.setAccessible(true)
f2.setAccessible(true)
val pid = f2.getLong(p)
pid
} finally {
f2.setAccessible(false)
f.setAccessible(false)
}
}
// If someone wants to add support for Windows processes,
// this would be the right place to do it:
case _ => throw new RuntimeException(
"Cannot get PID of a " + proc.getClass.getName)
}
} finally {
procField.setAccessible(false)
}
}
}
def runRouter(basePath: File, args: Array[String]): Unit = {
lazy val javaOpts = Seq(
"-Xmx512M",
"-Xms128m",
"-Dwrapper.logfile=/tmp/router.log",
"-Dwrapper.logfile.loglevel=DEBUG",
"-Dwrapper.java.pidfile=/tmp/routerjvm.pid",
"-Dwrapper.console.loglevel=DEBUG",
s"-Di2p.dir.base=${basePath}",
s"-Djava.library.path=${basePath}"
)
val javaOptsString = javaOpts.map(_ + " ").mkString
val cli = s"""java -cp "${new File(basePath, "lib").listFiles().map{f => f.toPath.toString.concat(":")}.mkString}." ${javaOptsString} net.i2p.router.Router"""
println(s"CLI => ${cli}")
val pb = Process(cli)
// Use "run" to let it fork in behind
val exitCode = pb.!
// ??? equals "throw not implemented" IIRC - it compiles at least :)
override def runRouter(args: Array[String]): Future[Process] = ???
def runRouter(basePath: File, args: Array[String]): Future[Process] = {
Future {
lazy val javaOpts = Seq(
"-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",
s"-Di2p.dir.base=${basePath}",
s"-Djava.library.path=${basePath}"
)
val javaOptsString = javaOpts.map(_ + " ").mkString
val cli = s"""java -cp "${new File(basePath, "lib").listFiles().map{f => f.toPath.toString.concat(":")}.mkString}." ${javaOptsString} net.i2p.router.Router"""
println(s"CLI => ${cli}")
val pb = Process(cli)
// Use "run" to let it fork in behind
pb.run
}
}
}

View File

@ -0,0 +1,27 @@
package net.i2p.launchers.osx
/**
*
*
* @author Meeh
* @since 0.9.35
*/
class SystemTrayManager {
object RouterState {
var isRunning: Boolean = false
var startupTime: Long = 0L
}
def isRunning = RouterState.isRunning
def setRunning(runs: Boolean): Unit = {
if (runs) setStartupTime()
RouterState.isRunning = runs
}
def setStartupTime() = {
RouterState.startupTime = System.currentTimeMillis / 1000
}
}