Compare commits

..

No commits in common. "master" and "v.0.1.0" have entirely different histories.

23 changed files with 109 additions and 489 deletions

1
.gitignore vendored
View file

@ -2,4 +2,3 @@
.idea/
target/
.DS_Store

View file

@ -1,4 +1,4 @@
version = 3.8.3
version = 3.7.4
runner.dialect = scala3

View file

@ -15,8 +15,6 @@ 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 -->
@ -35,10 +33,7 @@ the logic flow is too complex to correct without a significant rewrite.
Features
--------
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)
* Uses similar options to the original ImageJ Launcher, si IJP Launcher can be drop-in replacement
* Provides native executable for various OS/Hardware systems
- Windows
- Mac OS X Arm64 (Apple Silicon)
@ -53,7 +48,7 @@ Here are the futures that are already implemented (see release notes for futures
- 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
---------------------------------
@ -82,7 +77,7 @@ This example will show how to:
**1. Download FIJI without JRE**
Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
Go to https://fiji.sc/ and select "Download the no-JRE version".
That should get file called `fiji-nojre.zip`
**2. Unzip the `fiji-nojre.zip` in a folder of choice**
@ -100,49 +95,41 @@ 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 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)
* 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)
Click on `tar.gz` button to download and save to the `java` folder you created earlier.
You should have file like `OpenJDK11U-jre_aarch64_mac_hotspot_11.0.20_8.tar.gz`.
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.tar.gz`.
**5. Uncompress into the `Fiji.app/java` folder**
**5. Uncompress into the `java` folder**
That will create folder like `jdk-11.0.20+8-jre`.
That will create folder like `jdk-11.0.19+7-jre`.
This is the Java VM that IJP ImageJ Launcher will use to start Fiji.
**6. Download the IJP ImageJ Launcher and uncompress**
**6. Download the IJP ImageJ Launcher to the Fiji.app directory**
Go to [Releases], download "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip"
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.
Uncompress "IJP-ImageJ-Launcher-0.2.0-macosx-arm64.zip".
Inside you will get `ImageJ-macosx`.
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.
**7. Add to Fiji.app**
**7. Start ImageJ**
Inside `Fiji.app` locate folder `Contents/MacOS`.
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.
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
![macOS_warning_dialog_01.png](assets%2FmacOS_warning_dialog_01.png)
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
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.
If you have problems installing, please report in [Discussions] or [Image.sc Forum]
@ -156,7 +143,7 @@ This example will show how to:
**1. Download FIJI without JRE**
Go to https://imagej.net/software/fiji/downloads and download the **"No JRE"** version (not specific to any OS).
Go to https://fiji.sc/ and select "Download the no-JRE version".
That should get file called `fiji-nojre.zip`
**2. Unzip the `fiji-nojre.zip` in a folder of choice**
@ -173,31 +160,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 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)
* 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)
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.20_8.zip`.
You should have file like `OpenJDK11U-jre_x64_windows_hotspot_11.0.19_7.zip`.
**5. Uncompress into the `Fiji.app/java` folder**
**5. Uncompress into the `java` folder**
That will create folder like `jdk-11.0.20+8-jre`.
That will create folder like `jdk-11.0.19+7-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.2.0-windows_x64.exe", save it to the `Fiji.app` folder.
Go to [Releases], download "IJP-ImageJ-Launcher-0.1.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.2.0-windows_x64.exe`.
In the `Fiji.app` folder double-click on `IJP-ImageJ-Launcher-0.1.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.2.0-windows_x64.exe` and drag it to the Desktop.
**_Left_**-click on the `IJP-ImageJ-Launcher-0.1.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.
@ -207,13 +194,6 @@ 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

View file

@ -1,6 +1,6 @@
scalaVersion := "3.3.3"
scalaVersion := "3.3.0"
//name := "IJP-ImageJ-Launcher"
version := "0.2.0.1-SNAPSHOT"
version := "0.1.0"
versionScheme := Some("early-semver")
organization := "net.sf.ij-plugins"
homepage := Some(new URI("https://github.com/ij-plugins/ijp-imagej-launcher").toURL)
@ -16,26 +16,14 @@ enablePlugins(ScalaNativePlugin)
logLevel := Level.Info
libraryDependencies ++= Seq(
"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"
"com.github.scopt" %%% "scopt" % "4.1.0",
"com.lihaoyi" %%% "os-lib" % "0.9.1"
)
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 =>
@ -48,8 +36,3 @@ 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"

View file

@ -1,4 +1,4 @@
#!/bin/bash
DIR=$(cd "$(dirname "$0")" && pwd -P)
echo "$DIR"
"$DIR"/IJP-ImageJ-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"
"$DIR"/IJP-ImageL-Launcher-0.1.0-macosx-arm64 --debug --ij-dir "$DIR"

View file

@ -1,8 +0,0 @@
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

View file

@ -1,6 +1 @@
#
# Copyright (c) 2000-2023 Jarek Sacha. All Rights Reserved.
# Author's e-mail: jpsacha at gmail.com
#
sbt.version = 1.10.2
sbt.version = 1.8.3

View file

@ -1,2 +0,0 @@
// https://github.com/sbt/sbt-buildinfo
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0")

View file

@ -1,3 +1 @@
// https://github.com/scala-native/scala-native
resolvers ++= Resolver.sonatypeOssRepos("snapshots")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12")

View file

@ -1,40 +0,0 @@
#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
}

View file

@ -1,114 +0,0 @@
/*
* 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

View file

@ -1,73 +0,0 @@
/*
* 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

View file

@ -5,7 +5,6 @@
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
@ -13,7 +12,9 @@ import os.Path
import java.io.File
import java.lang.ProcessBuilder.Redirect
class Launcher(using logger: Logger):
class Launcher(logger: Logger):
private val jarsDirName = "jars"
def run(config: Config): Unit =
prepareLaunch(config) match
@ -28,16 +29,33 @@ class Launcher(using logger: Logger):
private def prepareLaunch(config: Config): Either[String, Seq[String]] =
for
ijDir <- IJDir.locate(config)
_ <- Updater.update(ijDir, config.dryRun)
ijConfig <- IJConfigFile.readFromDir(ijDir)
launcherJar <- findImageJLauncherJar(ijDir.toIO)
javaExe <- locateJavaExecutable(config, ijDir.toIO)
ijDir <- locateIJDir(config)
_ <- Updater.update(Path(ijDir), config.dryRun, logger)
launcherJar <- findImageJLauncherJar(ijDir)
javaExe <- locateJavaExecutable(config, ijDir)
systemType <- determineSystemType()
yield
val maxMemoryMB = determineMaxMemoryMB()
logger.info(s"Max memory to use: ${maxMemoryMB}MB")
buildCommandLine(ijDir.toIO, javaExe, launcherJar, systemType, maxMemoryMB)
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''")
private def findImageJLauncherJar(ijDir: File): Either[String, File] =
logger.debug("Looking for 'imagej-launcher*.jar'")
@ -101,7 +119,7 @@ class Launcher(using 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
@ -179,7 +197,6 @@ class Launcher(using logger: Logger):
"plugins",
"net.imagej.Main"
)
end buildCommandLine
private def launch(command: Seq[String]): Unit =
logger.debug("launchImageJ ...")

View file

@ -5,60 +5,20 @@
package ij_plugins.imagej_launcher
import ij_plugins.imagej_launcher.Logger.{Level, logToFile}
import os.Path
import java.nio.file.{Files, StandardOpenOption}
import scala.util.control.NonFatal
import ij_plugins.imagej_launcher.Logger.Level
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"

View file

@ -5,14 +5,15 @@
package ij_plugins.imagej_launcher
import scopt.OParser
import scopt.{DefaultOEffectSetup, OParser}
import java.io.File
object Main:
private var logger = new Logger()
private val AppName = "IJP-ImageJ-Launcher"
private val AppVersion = BuildInfo.version
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 val VersionMessage = s"v.$AppVersion"
private val AppDescription =
"""Native launcher for ImageJ2
@ -28,7 +29,6 @@ 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(using logger).run(config)
private def runLauncher(config: Config): Unit = new Launcher(logger).run(config)
case class Config(
logLevel: Logger.Level = Logger.Level.Error,

View file

@ -1,29 +1,9 @@
package ij_plugins.imagej_launcher
import os.{Path, up}
import scala.scalanative.unsafe
import scala.scalanative.unsafe.*
import scala.scalanative.unsafe.extern
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

View file

@ -1,11 +1,6 @@
/*
* 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
import os.{Path, RelPath}
import scala.util.control.NonFatal
@ -18,7 +13,7 @@ object Updater:
* @param logger configured logger
* @return Number of files processed or an error message.
*/
def update(ijDir: Path, dryRun: Boolean)(using logger: Logger): Either[String, Long] =
def update(ijDir: Path, dryRun: Boolean, logger: Logger): Either[String, Long] =
try
val updateDir = ijDir / "update"
// Count used only for debug info
@ -28,7 +23,8 @@ object Updater:
os.walk(updateDir)
.filter(os.isFile)
.foreach: src =>
val dst = ijDir / src.relativeTo(updateDir)
// val relativeDir = src.relativeTo(updateDir)
val dst = ijDir / relativeTo(src, updateDir)
if os.size(src) == 0 then
logger.debug(s"remove: $dst")
if !dryRun then os.remove(dst)
@ -39,7 +35,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)
if !dryRun then deleteEmptyDirs(updateDir, logger)
Right(count)
else
logger.info("No update found")
@ -49,11 +45,30 @@ object Updater:
ex.printStackTrace()
Left(s"Failed to perform update: ${ex.getMessage} - ${ex.getClass.getSimpleName}")
private def deleteEmptyDirs(dir: Path)(using logger: Logger): Unit =
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 =
logger.debug(s"Cleaning directory: $dir")
os.list(dir)
.filter(os.isDir)
.foreach(p => deleteEmptyDirs(p))
.foreach(p => deleteEmptyDirs(p, logger))
if os.list(dir).isEmpty then
logger.debug(s"Removing empty dir: $dir")

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import os.Path
@main
def updaterDemo(ijDir: String): Unit =
given logger: Logger = new Logger(Logger.Level.Debug)
Updater.update(Path(ijDir), dryRun = false) match
Updater.update(Path(ijDir), dryRun = false, new Logger(Logger.Level.Debug)) match
case Right(count) => println(s"Processed $count files")
case Left(error) => println(s"Failed with error: $error")

View file

@ -1,4 +0,0 @@
# ImageJ startup properties
maxheap.mb = 1024m
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
legacy.mode = false

View file

@ -1,4 +0,0 @@
# ImageJ startup properties
maxheap.mb = 1024
jvmargs = -XX:+HeapDumpOnOutOfMemoryError -Xincgc
legacy.mode = false