mirror of
https://github.com/ij-plugins/ijp-imagej-launcher.git
synced 2024-11-13 16:29:01 -08:00
Compare commits
41 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6d8daca7b | ||
|
|
bbff825c9b | ||
|
|
6998af7cec | ||
|
|
1e3797fe19 | ||
|
|
1003fd308d | ||
|
|
e07360ec65 | ||
|
|
abcc138ea0 | ||
|
|
0ca11d6645 | ||
|
|
c8dec57570 | ||
|
|
79ecae6091 | ||
|
|
c2043dc24c | ||
|
|
4babd726f8 | ||
|
|
cbb9829f8e | ||
|
|
0bca3977ad | ||
|
|
be57049c49 | ||
|
|
d842833e34 | ||
|
|
da5ffe2701 | ||
|
|
8f158a0fe7 | ||
|
|
bb18354923 | ||
|
|
ad54989711 | ||
|
|
4e3fbe32a8 | ||
|
|
8a417ceedb | ||
|
|
9a0cb694f7 | ||
|
|
95638a462a | ||
|
|
374a162f29 | ||
|
|
11b248fc1f | ||
|
|
ce0c49c515 | ||
|
|
f2bb93871f | ||
|
|
eb8b6a9a17 | ||
|
|
faf6a879e5 | ||
|
|
60ad1b1816 | ||
|
|
8718c9310c | ||
|
|
c30b853f64 | ||
|
|
47674de560 | ||
|
|
87882dc6e3 | ||
|
|
1d5b8887a3 | ||
|
|
1f2c915159 | ||
|
|
231540bce5 | ||
|
|
63b3ad6f52 | ||
|
|
e2b6e2d550 | ||
|
|
1312fa24b1 |
23 changed files with 489 additions and 109 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
|||
.idea/
|
||||
target/
|
||||
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version = 3.7.4
|
||||
version = 3.8.3
|
||||
|
||||
runner.dialect = scala3
|
||||
|
||||
|
|
|
|||
96
ReadMe.md
96
ReadMe.md
|
|
@ -15,6 +15,8 @@ IJP ImageJ Launcher is a clean implementation on the core function of starting [
|
|||
* [Installing Fiji on Mac OS X Arm64](#installing-fiji-on-mac-os-x-arm64)
|
||||
* [Installing Fiji on Windows x64](#installing-fiji-on-windows-x64)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
* [Start-up log `~/.ijp_imagej_launcher.log`](#start-up-log-ijpimagejlauncherlog)
|
||||
* [Starting from command prompt](#starting-from-command-prompt)
|
||||
* [Developer Setup](#developer-setup)
|
||||
|
||||
<!-- TOC -->
|
||||
|
|
@ -33,7 +35,10 @@ the logic flow is too complex to correct without a significant rewrite.
|
|||
Features
|
||||
--------
|
||||
|
||||
* Uses similar options to the original ImageJ Launcher, si IJP Launcher can be drop-in replacement
|
||||
Here are the futures that are already implemented (see release notes for futures ofa specific release):
|
||||
|
||||
* Uses similar options to the original ImageJ Launcher, so IJP Launcher can be used as a drop-in replacement
|
||||
* Intended to be used with Java 11 or newer (the original launcher can be used for Java 8)
|
||||
* Provides native executable for various OS/Hardware systems
|
||||
- Windows
|
||||
- Mac OS X Arm64 (Apple Silicon)
|
||||
|
|
@ -48,7 +53,7 @@ Features
|
|||
- Search ImageJ directory for available Java executables
|
||||
* Determines the amount of memory used by JVM based on total system memory use 75% of the max
|
||||
* Determines available `imagej-launcher*.jar`
|
||||
* Performs updates pending after the last time ImageJ was closed
|
||||
* **Performs updates** pending after the last time ImageJ was closed
|
||||
|
||||
Full List of Command Line Options
|
||||
---------------------------------
|
||||
|
|
@ -77,7 +82,7 @@ This example will show how to:
|
|||
|
||||
**1. Download FIJI without JRE**
|
||||
|
||||
Go to https://fiji.sc/ and select "Download the no-JRE version".
|
||||
Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
|
||||
That should get file called `fiji-nojre.zip`
|
||||
|
||||
**2. Unzip the `fiji-nojre.zip` in a folder of choice**
|
||||
|
|
@ -95,41 +100,49 @@ Inside the `Fiji.app` folder create a new folder called `java`.
|
|||
In browser open https://adoptium.net/temurin/releases/
|
||||
Select:
|
||||
|
||||
* operating system: `macOS`
|
||||
* architecture: `aarch64` also known as Apple Silicon or Arm64
|
||||
* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation)
|
||||
* version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it)
|
||||
* Operating System: `macOS`
|
||||
* Architecture: `aarch64` also known as Apple Silicon or Arm64
|
||||
* Package Type: `JRE` (`JDK` is fine too, is larger and supports Java compilation)
|
||||
* Version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it)
|
||||
|
||||
Click on `tar.gz` button to download and save to the `java` folder you created earlier.
|
||||
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.tar.gz`.
|
||||
You should have file like `OpenJDK11U-jre_aarch64_mac_hotspot_11.0.20_8.tar.gz`.
|
||||
|
||||
**5. Uncompress into the `java` folder**
|
||||
**5. Uncompress into the `Fiji.app/java` folder**
|
||||
|
||||
That will create folder like `jdk-11.0.19+7-jre`.
|
||||
That will create folder like `jdk-11.0.20+8-jre`.
|
||||
This is the Java VM that IJP ImageJ Launcher will use to start Fiji.
|
||||
|
||||
**6. Download the IJP ImageJ Launcher to the Fiji.app directory**
|
||||
**6. Download the IJP ImageJ Launcher and uncompress**
|
||||
|
||||
Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-macosx-arm64"
|
||||
and "IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command", save them to the `Fiji.app` folder.
|
||||
Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip"
|
||||
|
||||
The "*.command" file is a helper that can be used to launch Fiji without using command prompt.
|
||||
Future versions of the IJP Launcher, after v.0.1.0, may eliminate the need for using this file.
|
||||
Uncompress "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip".
|
||||
Inside you will get `ImageJ-macosx`.
|
||||
|
||||
**7. Start ImageJ**
|
||||
**7. Add to Fiji.app**
|
||||
|
||||
In the `Fiji.app` folder double-click on `IJP-ImageL-Launcher-0.1.0-macosx-arm64.command` file (note the extension "*
|
||||
.command")
|
||||
That should start Fiji.
|
||||
You may need to open Settings and allow the IJP ImageJ Launcher to run.
|
||||
Inside `Fiji.app` locate folder `Contents/MacOS`.
|
||||
|
||||
You can also create an alis on the Desktop to avoid navigating to the `Fiji.app` folder each time.
|
||||
Using Finder, press `Option`+`Command` and drag the *.command file to the Desktop.
|
||||
The original *.command file will stay were it is and a new icon/alias (wth a little arrow at the bottom) will be created
|
||||
on the Desktop.
|
||||
Now you can double-click on the new alias on the Desktop to start Fiji.
|
||||
You can rename the Desktop alias to whatever you like, for instance, `Fiji`, but do not change names of the downloaded
|
||||
files, otherwise the alias (and *.command) may no longer work, and you will need to use terminal to start the launcher.
|
||||
Copy `ImageJ-macosx` to the `Contents/MacOS` folder, replacing `ImageJ-macosx` that was there.
|
||||
|
||||
**8. Move Fiji.app to the Application folder**
|
||||
|
||||
At this point you can move the `Fiji.app` folder to the Applications folder and use is as a regular msOS application.
|
||||
|
||||
**9. Troubleshooting**
|
||||
|
||||
When you attempt to run Fiji with the new Launcher you may get a warning dialog
|
||||

|
||||
|
||||
Possible work-around
|
||||
|
||||
1. Delete `Fuji.app` folder
|
||||
2. Uncompressed `fiji-nojre.zip` to recreate `Fuji.app` folder, but do not make any changes to it yet. You may need to do it is different folder than before.
|
||||
3. Control-clock on `Fuji.app` and select "Open". You will see dialog saying
|
||||
"macOS cannot verify the developer of “Fiji”. Are you sure you want to open it?"
|
||||
4. Click on "Open". You will see Fiji logo, but the application will close since it is not setup yet
|
||||
5. Now you can repeat steps "3. Create place for Java (JRE)" to "7. Add to Fiji.app" above
|
||||
|
||||
If you have problems installing, please report in [Discussions] or [Image.sc Forum]
|
||||
|
||||
|
|
@ -143,7 +156,7 @@ This example will show how to:
|
|||
|
||||
**1. Download FIJI without JRE**
|
||||
|
||||
Go to https://fiji.sc/ and select "Download the no-JRE version".
|
||||
Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
|
||||
That should get file called `fiji-nojre.zip`
|
||||
|
||||
**2. Unzip the `fiji-nojre.zip` in a folder of choice**
|
||||
|
|
@ -160,31 +173,31 @@ Inside the `Fiji.app` folder create a new folder called `java`.
|
|||
In browser open https://adoptium.net/temurin/releases/
|
||||
Select:
|
||||
|
||||
* operating system: `Windows`
|
||||
* architecture: `x64` also known as Apple Silicon or Arm64
|
||||
* package: `JRE` (`JDK` is fine too, is larger and supports Java compilation)
|
||||
* version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it)
|
||||
* Operating System: `Windows`
|
||||
* Architecture: `x64` also known as Apple Silicon or Arm64
|
||||
* Package Type: `JRE` (`JDK` is fine too, is larger and supports Java compilation)
|
||||
* Version: `11-LTS` (`17-LTS` will work too, but you will not have JavaScript available, if you want to use it)
|
||||
|
||||
Click on `.zip` button to download and save to the `java` folder you created earlier.
|
||||
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.zip`.
|
||||
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.20_8.zip`.
|
||||
|
||||
**5. Uncompress into the `java` folder**
|
||||
**5. Uncompress into the `Fiji.app/java` folder**
|
||||
|
||||
That will create folder like `jdk-11.0.19+7-jre`.
|
||||
That will create folder like `jdk-11.0.20+8-jre`.
|
||||
This is the Java VM that IJP ImageJ Launcher will use to start Fiji.
|
||||
|
||||
**6. Download the IJP ImageJ Launcher to the Fiji.app directory**
|
||||
|
||||
Go to [Releases], download "IJP-ImageJ-Launcher-0.1.0-windows_x64.exe", save it to the `Fiji.app` folder.
|
||||
Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-windows_x64.exe", save it to the `Fiji.app` folder.
|
||||
|
||||
**7. Start ImageJ**
|
||||
|
||||
In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.1.0-windows_x64.exe`.
|
||||
In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.2.0-windows_x64.exe`.
|
||||
That should start Fiji.
|
||||
|
||||
You can also create a shortcut on the Desktop to avoid navigating to the `Fiji.app` folder each time.
|
||||
|
||||
**_Left_**-click on the `IJP-ImageJ-Launcher-0.1.0-windows_x64.exe` and drag it to the Desktop.
|
||||
**_Left_**-click on the `IJP-ImageJ-Launcher-0.2.0-windows_x64.exe` and drag it to the Desktop.
|
||||
Once you release mouse button, a pop-up manu will open, select "Create shortcut here".
|
||||
Now you can double-click on the new shortcut on the Desktop to start Fiji.
|
||||
|
||||
|
|
@ -194,6 +207,13 @@ If you have problems installing, please report in [Discussions] or [Image.sc For
|
|||
|
||||
### Troubleshooting
|
||||
|
||||
#### Start-up log `~/.ijp_imagej_launcher.log`
|
||||
|
||||
The IJP-ImageJ-Launcher writes diagnostic info to a file `.ijp_imagej_launcher.log` in the users home directory.
|
||||
The information recorded is some as using `--debug` on command line.
|
||||
|
||||
#### Starting from command prompt
|
||||
|
||||
You can start the IJP Image Launcher from the terminal and see diagnostic printouts that may help troubleshoot potential
|
||||
issues.
|
||||
|
||||
|
|
|
|||
BIN
assets/macOS_warning_dialog_01.png
Normal file
BIN
assets/macOS_warning_dialog_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
27
build.sbt
27
build.sbt
|
|
@ -1,6 +1,6 @@
|
|||
scalaVersion := "3.3.0"
|
||||
scalaVersion := "3.3.3"
|
||||
//name := "IJP-ImageJ-Launcher"
|
||||
version := "0.1.0"
|
||||
version := "0.2.0.1-SNAPSHOT"
|
||||
versionScheme := Some("early-semver")
|
||||
organization := "net.sf.ij-plugins"
|
||||
homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL)
|
||||
|
|
@ -16,14 +16,26 @@ enablePlugins(ScalaNativePlugin)
|
|||
logLevel := Level.Info
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"com.github.scopt" %%% "scopt" % "4.1.0",
|
||||
"com.lihaoyi" %%% "os-lib" % "0.9.1"
|
||||
"com.github.scopt" %%% "scopt" % "4.1.0",
|
||||
"com.lihaoyi" %%% "os-lib" % "0.9.3",
|
||||
"org.scalatest" %%% "scalatest" % "3.2.18" % Test
|
||||
)
|
||||
|
||||
scalacOptions ++= Seq(
|
||||
"-unchecked",
|
||||
"-deprecation",
|
||||
"-explain",
|
||||
"-explain-types",
|
||||
"-rewrite",
|
||||
"-source:3.3-migration",
|
||||
// "-Wvalue-discard",
|
||||
"-Wunused:all"
|
||||
)
|
||||
|
||||
Compile / run / mainClass := Some("ij_plugins.imagej_launcher.Main")
|
||||
|
||||
// import to add Scala Native options
|
||||
import scala.scalanative.build._
|
||||
import scala.scalanative.build.*
|
||||
|
||||
// defaults set with common options shown
|
||||
nativeConfig ~= { c =>
|
||||
|
|
@ -36,3 +48,8 @@ nativeConfig ~= { c =>
|
|||
//nativeConfig ~= { c =>
|
||||
// c.withCompileOptions(c.compileOptions ++ Seq("-v"))
|
||||
//}
|
||||
|
||||
// Version info generation from SBT configuration
|
||||
enablePlugins(BuildInfoPlugin)
|
||||
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion)
|
||||
buildInfoPackage := "ij_plugins.imagej_launcher"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
DIR=$(cd "$(dirname "$0")" && pwd -P)
|
||||
echo "$DIR"
|
||||
"$DIR"/IJP-ImageL-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"
|
||||
"$DIR"/IJP-ImageJ-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"
|
||||
8
notes/v.0.2.0.md
Normal file
8
notes/v.0.2.0.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Feature release: support better system integration on macOS - support installing ImageJ/Fiji in the application
|
||||
directory.
|
||||
See macOS installation info in the [ReadMe](https://github.com/ij-plugins/ijp-imagej-launcher)
|
||||
|
||||
* Use launcher location to infer `ij-dir`. #5
|
||||
* Session log is saved to `~/.ijp_imagej_launcher.log` to facilitate troubleshooting when not running from command
|
||||
prompt. The log is reset for each session. #6
|
||||
* Better inference of ImageJ directory on macOS - consider launcher being in subdirectory "Contents/MacOS" #7
|
||||
|
|
@ -1 +1,6 @@
|
|||
sbt.version = 1.8.3
|
||||
#
|
||||
# Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
# Author's e-mail: jpsacha at gmail.com
|
||||
#
|
||||
|
||||
sbt.version = 1.10.2
|
||||
|
|
|
|||
2
project/buildinfo.sbt
Normal file
2
project/buildinfo.sbt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// https://github.com/sbt/sbt-buildinfo
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0")
|
||||
|
|
@ -1 +1,3 @@
|
|||
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12")
|
||||
// https://github.com/scala-native/scala-native
|
||||
resolvers ++= Resolver.sonatypeOssRepos("snapshots")
|
||||
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
|
||||
|
|
|
|||
40
src/main/resources/scala-native/argv0.c
Normal file
40
src/main/resources/scala-native/argv0.c
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
#include <stdio.h>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <stdint.h>
|
||||
#include <sys/syslimits.h>
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
size_t path_max() {
|
||||
#ifdef _WIN32
|
||||
return MAX_PATH;
|
||||
#else
|
||||
return PATH_MAX + 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void get_exe_path(char* exe_path, size_t size) {
|
||||
|
||||
exe_path[0] = 0;
|
||||
|
||||
#if defined(__linux__)
|
||||
// char arg1[20];
|
||||
// sprintf( arg1, "/proc/%d/exe", getpid() );
|
||||
// printf("arg1 = %d\n", arg1);
|
||||
// readlink( arg1, exepath, PATH_MAX );
|
||||
readlink( "/proc/self/exe", exe_path, size );
|
||||
#elif defined(__APPLE__)
|
||||
if (_NSGetExecutablePath(exe_path, &size) != 0)
|
||||
printf("ERROR: buffer too small; need size %lu\n", size);
|
||||
#elif defined(_WIN32)
|
||||
unsigned int len = GetModuleFileNameA(GetModuleHandleA(0x0), exe_path, size);
|
||||
if (len == 0) // memory not sufficient or general error occurred
|
||||
printf("ERROR: buffer too small or general error.\n");
|
||||
#endif
|
||||
}
|
||||
114
src/main/scala/ij_plugins/imagej_launcher/IJConfigFile.scala
Normal file
114
src/main/scala/ij_plugins/imagej_launcher/IJConfigFile.scala
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
* Author's e-mail: jpsacha at gmail.com
|
||||
*/
|
||||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import os.Path
|
||||
|
||||
object IJConfigFile:
|
||||
|
||||
private val FileName: String = "ImageJ.cfg"
|
||||
private val MagicHeader: String = "# ImageJ startup properties"
|
||||
private val MaxHeapMBKey: String = "maxheap.mb"
|
||||
private val JvmArgsKey: String = "jvmargs"
|
||||
private val LegacyModeKey: String = "legacy.mode"
|
||||
private val AssignmentSymbol: String = "="
|
||||
private val CommentSymbol: String = "#"
|
||||
|
||||
/**
|
||||
* Read `ImageJ.cfg` from a specified directory.
|
||||
* The configuration file must be in ImageJ2 format (with magic header "# ImageJ startup properties").
|
||||
*
|
||||
* @param dir directory where `ImageJ.cfg` is located
|
||||
* @param logger logger
|
||||
* @return option containing the configuration file (Right).
|
||||
* The option is empty if file is not present in input `dir`.
|
||||
* An error message is returned if file is present but cannot be read without errors (Left).
|
||||
*/
|
||||
def readFromDir(dir: Path)(using logger: Logger): Either[String, Option[IJCConfig]] =
|
||||
logger.debug(s"Reading ImageJ configuration from directory: $dir")
|
||||
val path = dir / FileName
|
||||
logger.debug(s" Looking for $FileName in: '$path'")
|
||||
readFromFile(path)
|
||||
|
||||
private[imagej_launcher] def readFromFile(ijConfigFile: Path)(using logger: Logger): Either[String, Option[IJCConfig]] =
|
||||
if os.exists(ijConfigFile) then
|
||||
val lines = os.read.lines(ijConfigFile)
|
||||
val cfgE =
|
||||
lines.headOption match
|
||||
case Some(line) =>
|
||||
if line.trim.startsWith(MagicHeader) then
|
||||
logger.debug(s" Parsing $FileName")
|
||||
for
|
||||
props <- parseProps(lines)
|
||||
maxHeapMB <- parseLong(props, MaxHeapMBKey)
|
||||
jvmArgs <- parseString(props, JvmArgsKey)
|
||||
legacyMode <- parseBoolean(props, LegacyModeKey)
|
||||
yield Option(IJCConfig(maxHeapMB, jvmArgs, legacyMode))
|
||||
else
|
||||
Left(s"$FileName is invalid, does not contain header: '$MagicHeader'")
|
||||
case None =>
|
||||
Left(s"$FileName is empty.")
|
||||
|
||||
// Add context to the error message, if there is one
|
||||
cfgE.left.map(e => s"Failed to read $FileName. $e")
|
||||
else
|
||||
logger.debug(s" $FileName not found: $ijConfigFile")
|
||||
Right(None)
|
||||
|
||||
private[imagej_launcher] def parseProps(lines: Seq[String]): Either[String, Map[String, String]] =
|
||||
// Parse each line
|
||||
val lineResults: Seq[Either[String, (String, String)]] =
|
||||
lines
|
||||
.zipWithIndex
|
||||
.filter { case (line, _) => !line.startsWith(CommentSymbol) }
|
||||
.map { case (line, lineNumber) =>
|
||||
val index = line.indexOf(AssignmentSymbol)
|
||||
if index > 0 then
|
||||
val key = line.substring(0, index).trim
|
||||
if !key.isBlank then
|
||||
Right(key -> line.substring(index + 1).trim)
|
||||
else
|
||||
Left(s"Error in line $lineNumber: key cannot be empty: '$line'")
|
||||
else if index == 0 then
|
||||
Left(s"Error in line $lineNumber: line cannot start with an assignment symbol '$AssignmentSymbol': '$line'")
|
||||
else
|
||||
Left(s"Error in line $lineNumber: no assignment symbol '$AssignmentSymbol' present: '$line'")
|
||||
}
|
||||
|
||||
// Separate lines with errors
|
||||
val errors = lineResults.collect { case e: Left[?, ?] => e }
|
||||
if errors.isEmpty then
|
||||
// Convert key->value pairs to a map
|
||||
val r =
|
||||
lineResults
|
||||
.collect { case e: Right[?, ?] => e }
|
||||
.map(_.value)
|
||||
.toMap
|
||||
Right(r)
|
||||
else
|
||||
Left(errors.map(_.value).mkString("\n"))
|
||||
|
||||
private def parseLong(props: Map[String, String], key: String): Either[String, Long] =
|
||||
parseString(props, key).flatMap: s =>
|
||||
s.toLongOption match
|
||||
case Some(v) => Right(v)
|
||||
case None => Left(s"Failed to parse '$s' as an integer.")
|
||||
|
||||
private def parseString(props: Map[String, String], key: String): Either[String, String] =
|
||||
props.get(key) match
|
||||
case Some(v) => Right(v)
|
||||
case None => Left(s"No key named '$key'.")
|
||||
|
||||
private def parseBoolean(props: Map[String, String], key: String): Either[String, Boolean] =
|
||||
parseString(props, key).flatMap: s =>
|
||||
s.toBooleanOption match
|
||||
case Some(v) => Right(v)
|
||||
case None => Left(s"Failed to parse '$s' as a boolean.")
|
||||
|
||||
/** Configuration loaded from `ImageJ.cfg`. */
|
||||
case class IJCConfig(maxHeapMB: Long, jvmArgs: String, legacyMode: Boolean)
|
||||
|
||||
end IJConfigFile
|
||||
73
src/main/scala/ij_plugins/imagej_launcher/IJDir.scala
Normal file
73
src/main/scala/ij_plugins/imagej_launcher/IJDir.scala
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
* Author's e-mail: jpsacha at gmail.com
|
||||
*/
|
||||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import ij_plugins.imagej_launcher.Main.Config
|
||||
import os.{FilePath, Path}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
object IJDir:
|
||||
|
||||
/** Name of an ImageJ sub-directory containing jars */
|
||||
val jarsDirName = "jars"
|
||||
|
||||
/** Locate ImageJ directory */
|
||||
def locate(config: Config)(using logger: Logger): Either[String, Path] =
|
||||
logger.debug("Looking for ImageJ directory")
|
||||
|
||||
config.ijDir match
|
||||
case Some(d) =>
|
||||
logger.debug(s" Considering provided ij-dir: '$d'")
|
||||
asPath(d.getPath).flatMap: p =>
|
||||
logger.debug(s" '$p'")
|
||||
if isIJDir(p) then Right(p)
|
||||
else Left(s"ij-dir is not an ImageJ directory [$p]")
|
||||
case None =>
|
||||
logger.debug(" Considering application directory")
|
||||
val appPath = Native.applicationPath()
|
||||
logger.debug(s" Application directory: '$appPath'")
|
||||
inferIJDir(appPath) match
|
||||
case Some(p) =>
|
||||
Right(p)
|
||||
case None =>
|
||||
logger.debug(" Application directory is not an ImageJ directory")
|
||||
logger.debug(" Considering current working directory")
|
||||
val cwd = os.pwd
|
||||
logger.debug(s" Current working directory: '$cwd'")
|
||||
inferIJDir(cwd) match
|
||||
case Some(p) =>
|
||||
Right(p)
|
||||
case None =>
|
||||
logger.debug(" Current working directory is not an ImageJ directory.")
|
||||
Left("Cannot locate ImageJ directory.")
|
||||
|
||||
private def inferIJDir(path: Path): Option[Path] =
|
||||
if isIJDir(path) then
|
||||
Option(path)
|
||||
else if path.endsWith(os.rel / "Contents" / "MacOS") then
|
||||
Option(path / os.up / os.up)
|
||||
else
|
||||
None
|
||||
|
||||
private def isIJDir(path: Path): Boolean =
|
||||
os.exists(path) &&
|
||||
os.isDir(path) &&
|
||||
os.list(path).exists(f => f.last == jarsDirName && os.isDir(f))
|
||||
|
||||
private def asPath(filePath: String): Either[String, Path] =
|
||||
if filePath.trim.startsWith("~") then
|
||||
try
|
||||
Right(Path.expandUser(filePath))
|
||||
catch
|
||||
case NonFatal(ex) =>
|
||||
Left(s"Not an absolute path: '$filePath' [${ex.getMessage}]")
|
||||
else
|
||||
FilePath(filePath) match
|
||||
case p: Path => Right(p)
|
||||
case _ => Left(s"Not an absolute path: '$filePath'")
|
||||
|
||||
end IJDir
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import ij_plugins.imagej_launcher.IJDir.jarsDirName
|
||||
import ij_plugins.imagej_launcher.Launcher.javaExeFileName
|
||||
import ij_plugins.imagej_launcher.Main.Config
|
||||
import os.Path
|
||||
|
|
@ -12,9 +13,7 @@ import os.Path
|
|||
import java.io.File
|
||||
import java.lang.ProcessBuilder.Redirect
|
||||
|
||||
class Launcher(logger: Logger):
|
||||
|
||||
private val jarsDirName = "jars"
|
||||
class Launcher(using logger: Logger):
|
||||
|
||||
def run(config: Config): Unit =
|
||||
prepareLaunch(config) match
|
||||
|
|
@ -29,33 +28,16 @@ class Launcher(logger: Logger):
|
|||
|
||||
private def prepareLaunch(config: Config): Either[String, Seq[String]] =
|
||||
for
|
||||
ijDir <- locateIJDir(config)
|
||||
_ <- Updater.update(Path(ijDir), config.dryRun, logger)
|
||||
launcherJar <- findImageJLauncherJar(ijDir)
|
||||
javaExe <- locateJavaExecutable(config, ijDir)
|
||||
ijDir <- IJDir.locate(config)
|
||||
_ <- Updater.update(ijDir, config.dryRun)
|
||||
ijConfig <- IJConfigFile.readFromDir(ijDir)
|
||||
launcherJar <- findImageJLauncherJar(ijDir.toIO)
|
||||
javaExe <- locateJavaExecutable(config, ijDir.toIO)
|
||||
systemType <- determineSystemType()
|
||||
yield
|
||||
val maxMemoryMB = determineMaxMemoryMB()
|
||||
logger.info(s"Max memory to use: ${maxMemoryMB}MB")
|
||||
buildCommandLine(ijDir, javaExe, launcherJar, systemType, maxMemoryMB)
|
||||
|
||||
private def locateIJDir(config: Config): Either[String, File] =
|
||||
logger.debug("Looking for ImageJ directory")
|
||||
|
||||
val dir = config.ijDir match
|
||||
case Some(d) =>
|
||||
logger.debug(" Considering provided ij-dir")
|
||||
d
|
||||
case None =>
|
||||
logger.debug(" Considering current directory")
|
||||
new File(".").getCanonicalFile
|
||||
|
||||
dir.listFiles().find(f => f.getName == jarsDirName && f.isDirectory) match
|
||||
case Some(_) =>
|
||||
logger.info(s" ImageJ directory set to: '$dir'")
|
||||
Right(dir)
|
||||
case None =>
|
||||
Left(s"Cannot locate ImageJ directory. No subdirectory '$jarsDirName' in '$dir''")
|
||||
buildCommandLine(ijDir.toIO, javaExe, launcherJar, systemType, maxMemoryMB)
|
||||
|
||||
private def findImageJLauncherJar(ijDir: File): Either[String, File] =
|
||||
logger.debug("Looking for 'imagej-launcher*.jar'")
|
||||
|
|
@ -119,7 +101,7 @@ class Launcher(logger: Logger):
|
|||
p.toIO.getName == javaExeFileName &&
|
||||
p.toIO.getParentFile.getName == "bin"
|
||||
)
|
||||
logger.debug(" Candidates: " + c2.mkString(", "))
|
||||
logger.debug(" Candidates: " + c2.mkString(", "))
|
||||
c2.map(_.toIO.getParentFile.getParentFile)
|
||||
.headOption
|
||||
else
|
||||
|
|
@ -197,6 +179,7 @@ class Launcher(logger: Logger):
|
|||
"plugins",
|
||||
"net.imagej.Main"
|
||||
)
|
||||
end buildCommandLine
|
||||
|
||||
private def launch(command: Seq[String]): Unit =
|
||||
logger.debug("launchImageJ ...")
|
||||
|
|
|
|||
|
|
@ -5,20 +5,60 @@
|
|||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import ij_plugins.imagej_launcher.Logger.Level
|
||||
import ij_plugins.imagej_launcher.Logger.{Level, logToFile}
|
||||
import os.Path
|
||||
|
||||
import java.nio.file.{Files, StandardOpenOption}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
class Logger(val level: Level = Level.Info):
|
||||
|
||||
def debug(msg: String): Unit = pprint(Level.Debug, msg)
|
||||
def info(msg: String): Unit = pprint(Level.Info, msg)
|
||||
def error(msg: String): Unit = pprint(Level.Error, msg)
|
||||
|
||||
private def pprint(l: Level, message: String): Unit =
|
||||
val m = f"${l.name}%-5s: $message"
|
||||
if l.level <= level.level then println(f"${l.name}%-5s: $message")
|
||||
logToFile(m)
|
||||
|
||||
object Logger:
|
||||
|
||||
private val LogFile: Path = os.home / ".ijp_imagej_launcher.log"
|
||||
private var logFileReset: Boolean = true
|
||||
|
||||
enum Level(val level: Int, val name: String):
|
||||
case Off extends Level(0, "OFF")
|
||||
case Error extends Level(200, "ERROR")
|
||||
case Info extends Level(400, "INFO")
|
||||
case Debug extends Level(500, "DEBUG")
|
||||
case All extends Level(Int.MaxValue, "DEBUG")
|
||||
|
||||
private def logToFile(message: String): Unit =
|
||||
try
|
||||
if logFileReset && os.exists(LogFile) then
|
||||
val _ = os.remove(LogFile)
|
||||
logFileReset = false
|
||||
|
||||
val data = readableTimeStamp() + ": " + message + "\n"
|
||||
val lines = data.getBytes()
|
||||
val _ = Files.write(
|
||||
LogFile.toNIO,
|
||||
lines,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND,
|
||||
StandardOpenOption.WRITE
|
||||
)
|
||||
catch
|
||||
case NonFatal(_) =>
|
||||
// Ignore exceptions
|
||||
|
||||
private def readableTimeStamp(): String =
|
||||
// Simple implementation returns GMT time (no date)
|
||||
// Use custom code as DateTimeFormatter is not available in Scala Native 0.4.14
|
||||
val t = System.currentTimeMillis()
|
||||
val sss = t % 1000
|
||||
val ss = (t / 1000L).toInt % 60
|
||||
val mm = (t / (1000L * 60L)).toInt % 60
|
||||
val hh = (t / (1000L * 60L * 60L)).toInt % 24
|
||||
f"$hh%02d-$mm%02d-$ss%02d_$sss%03dZ"
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@
|
|||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import scopt.{DefaultOEffectSetup, OParser}
|
||||
import scopt.OParser
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Main:
|
||||
private var logger = new Logger()
|
||||
private val AppName = "ijp-imagej-launcher"
|
||||
// private val AppVersion = s"${Version.version} [${Version.buildTimeStr}]"
|
||||
private val AppVersion = s"0.1.0"
|
||||
private var logger = new Logger()
|
||||
private val AppName = "IJP-ImageJ-Launcher"
|
||||
private val AppVersion = BuildInfo.version
|
||||
private val VersionMessage = s"v.$AppVersion"
|
||||
private val AppDescription =
|
||||
"""Native launcher for ImageJ2
|
||||
|
|
@ -29,6 +28,7 @@ object Main:
|
|||
case None =>
|
||||
|
||||
private def parseCommandLine(args: Array[String]): Option[Config] =
|
||||
logger.debug("Command line: " + args.map("'" + _ + "'").mkString(", "))
|
||||
val builder = OParser.builder[Config]
|
||||
val parser1 =
|
||||
import builder.*
|
||||
|
|
@ -69,7 +69,7 @@ object Main:
|
|||
|
||||
private def setupLogger(logLevel: Logger.Level): Unit = logger = new Logger(logLevel)
|
||||
|
||||
private def runLauncher(config: Config): Unit = new Launcher(logger).run(config)
|
||||
private def runLauncher(config: Config): Unit = new Launcher(using logger).run(config)
|
||||
|
||||
case class Config(
|
||||
logLevel: Logger.Level = Logger.Level.Error,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,29 @@
|
|||
package ij_plugins.imagej_launcher
|
||||
|
||||
import scala.scalanative.unsafe.extern
|
||||
import os.{Path, up}
|
||||
|
||||
import scala.scalanative.unsafe
|
||||
import scala.scalanative.unsafe.*
|
||||
|
||||
object Native:
|
||||
|
||||
def applicationPath(): Path =
|
||||
val maxPath: CSize = argv0.path_max()
|
||||
// use Zone to manage native memory
|
||||
val exePath =
|
||||
Zone { implicit z =>
|
||||
val buffer: CString = alloc[CChar](maxPath)
|
||||
argv0.get_exe_path(buffer, maxPath)
|
||||
unsafe.fromCString(buffer)
|
||||
}
|
||||
Path(exePath) / up
|
||||
|
||||
@extern
|
||||
object mem:
|
||||
def determineTotalSystemMemory(): Long = extern
|
||||
|
||||
@extern
|
||||
private object argv0:
|
||||
def path_max(): CSize = extern
|
||||
|
||||
def get_exe_path(exe_path: CString, size: CSize): Unit = extern
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
/*
|
||||
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
* Author's e-mail: jpsacha at gmail.com
|
||||
*/
|
||||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import os.{Path, RelPath}
|
||||
import os.Path
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
|
|
@ -13,7 +18,7 @@ object Updater:
|
|||
* @param logger configured logger
|
||||
* @return Number of files processed or an error message.
|
||||
*/
|
||||
def update(ijDir: Path, dryRun: Boolean, logger: Logger): Either[String, Long] =
|
||||
def update(ijDir: Path, dryRun: Boolean)(using logger: Logger): Either[String, Long] =
|
||||
try
|
||||
val updateDir = ijDir / "update"
|
||||
// Count used only for debug info
|
||||
|
|
@ -23,8 +28,7 @@ object Updater:
|
|||
os.walk(updateDir)
|
||||
.filter(os.isFile)
|
||||
.foreach: src =>
|
||||
// val relativeDir = src.relativeTo(updateDir)
|
||||
val dst = ijDir / relativeTo(src, updateDir)
|
||||
val dst = ijDir / src.relativeTo(updateDir)
|
||||
if os.size(src) == 0 then
|
||||
logger.debug(s"remove: $dst")
|
||||
if !dryRun then os.remove(dst)
|
||||
|
|
@ -35,7 +39,7 @@ object Updater:
|
|||
if !dryRun then os.move(src, dst, replaceExisting = true, createFolders = true)
|
||||
count += 1
|
||||
logger.debug(s"Delete update directory: $updateDir")
|
||||
if !dryRun then deleteEmptyDirs(updateDir, logger)
|
||||
if !dryRun then deleteEmptyDirs(updateDir)
|
||||
Right(count)
|
||||
else
|
||||
logger.info("No update found")
|
||||
|
|
@ -45,30 +49,11 @@ object Updater:
|
|||
ex.printStackTrace()
|
||||
Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}")
|
||||
|
||||
private def relativeTo(src: Path, base: Path): RelPath =
|
||||
// This does what src.relativeTo(base) should do
|
||||
// Problems is in the native code on Windows,
|
||||
// os.Path#relativeTo creates relative path by adding `../` at the beginning of the absolute path,
|
||||
// co you may get `../C:\a\b` which leads to a exception soon after.
|
||||
// The issue is with Scala Native implementation of java.nio.file.Path#relativize on Windows,
|
||||
// See https://github.com/scala-native/scala-native/issues/3293
|
||||
|
||||
// This implementation is very limited but sufficient for our use.
|
||||
// It assumes specific relation between src and base.
|
||||
|
||||
val srcStr = src.toString
|
||||
val baseStr = base.toString
|
||||
require(baseStr.nonEmpty)
|
||||
require(srcStr.startsWith(baseStr))
|
||||
|
||||
val relStr = srcStr.drop(baseStr.length + 1)
|
||||
RelPath(relStr)
|
||||
|
||||
private def deleteEmptyDirs(dir: Path, logger: Logger): Unit =
|
||||
private def deleteEmptyDirs(dir: Path)(using logger: Logger): Unit =
|
||||
logger.debug(s"Cleaning directory: $dir")
|
||||
os.list(dir)
|
||||
.filter(os.isDir)
|
||||
.foreach(p => deleteEmptyDirs(p, logger))
|
||||
.foreach(p => deleteEmptyDirs(p))
|
||||
|
||||
if os.list(dir).isEmpty then
|
||||
logger.debug(s"Removing empty dir: $dir")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
* Author's e-mail: jpsacha at gmail.com
|
||||
*/
|
||||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
|
||||
class IJConfigFileTest extends AnyFlatSpec:
|
||||
|
||||
given logger:Logger = new Logger(Logger.Level.All)
|
||||
|
||||
"IJConfigFile" should "read valid config files" in:
|
||||
val path = os.pwd / "test" / "data" / "config_valid" / "ImageJ.cfg"
|
||||
|
||||
IJConfigFile.readFromFile(path) match
|
||||
case Right(cfg) =>
|
||||
logger.debug(cfg.toString)
|
||||
assert(cfg.nonEmpty)
|
||||
case Left(err) =>
|
||||
logger.debug(s"Unexpected error: $err")
|
||||
fail()
|
||||
|
||||
it should "fail reading invalid config files" in:
|
||||
val path = os.pwd / "test" / "data" / "config_invalid" / "ImageJ.cfg"
|
||||
|
||||
IJConfigFile.readFromFile(path) match
|
||||
case Right(cfg) =>
|
||||
logger.debug(cfg.toString)
|
||||
fail(s"Expected to return errors when reading config, but read config as: $cfg")
|
||||
case Left(err) =>
|
||||
logger.debug(s"Expected error\n:$err")
|
||||
28
src/test/scala/ij_plugins/imagej_launcher/PathSpec.scala
Normal file
28
src/test/scala/ij_plugins/imagej_launcher/PathSpec.scala
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
|
||||
* Author's e-mail: jpsacha at gmail.com
|
||||
*/
|
||||
|
||||
package ij_plugins.imagej_launcher
|
||||
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should
|
||||
|
||||
import java.nio.file.FileSystems
|
||||
import scala.scalanative.runtime.Platform.isWindows
|
||||
|
||||
class PathSpec extends AnyFlatSpec with should.Matchers:
|
||||
|
||||
"A Path" should "should `relativize` jars on Windows (Scala Native #3293)" in {
|
||||
if isWindows() then
|
||||
// This to test that fix for Scala Native #3293 implemented, it should be part of Scala Native 0.4.13
|
||||
// See https://github.com/scala-native/scala-native/issues/3293
|
||||
|
||||
// val src = Path.of("C:\\a\\b\\c.jar")
|
||||
val src = FileSystems.getDefault.getPath("C:\\a\\b\\c.jar")
|
||||
// val base = Path.of("C:\\a")
|
||||
val base = FileSystems.getDefault.getPath("C:\\a")
|
||||
|
||||
val rel = base.relativize(src)
|
||||
rel.toString should be("b\\c.jar")
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import os.Path
|
|||
|
||||
@main
|
||||
def updaterDemo(ijDir: String): Unit =
|
||||
Updater.update(Path(ijDir), dryRun = false, new Logger(Logger.Level.Debug)) match
|
||||
given logger: Logger = new Logger(Logger.Level.Debug)
|
||||
Updater.update(Path(ijDir), dryRun = false) match
|
||||
case Right(count) => println(s"Processed $count files")
|
||||
case Left(error) => println(s"Failed with error: $error")
|
||||
|
|
|
|||
4
test/data/config_invalid/ImageJ.cfg
Normal file
4
test/data/config_invalid/ImageJ.cfg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ImageJ startup properties
|
||||
maxheap.mb = 1024m
|
||||
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
|
||||
legacy.mode = false
|
||||
4
test/data/config_valid/ImageJ.cfg
Normal file
4
test/data/config_valid/ImageJ.cfg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ImageJ startup properties
|
||||
maxheap.mb = 1024
|
||||
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
|
||||
legacy.mode = false
|
||||
Loading…
Add table
Add a link
Reference in a new issue