mirror of
https://github.com/ij-plugins/ijp-imagej-launcher.git
synced 2024-11-13 16:29:01 -08:00
Merge branch 'master' into release
This commit is contained in:
commit
4e3fbe32a8
20 changed files with 445 additions and 74 deletions
24
ReadMe.md
24
ReadMe.md
|
|
@ -33,7 +33,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 +51,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
|
||||
---------------------------------
|
||||
|
|
@ -116,9 +119,22 @@ and "IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command", save them to the `Fiji.app
|
|||
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. Start ImageJ**
|
||||
**7. Set Executable Permissions**
|
||||
|
||||
In the `Fiji.app` folder double-click on `IJP-ImageL-Launcher-0.1.0-macosx-arm64.command` file (note the extension "*
|
||||
When you download launcher files they may be saved without executable permissions.
|
||||
|
||||
* Open terminal
|
||||
* Navigate to the Fiji.app folder, for instance, `cd ~/Download/Fiji.app`
|
||||
* Add executable permission to the launcher and the "*.command" file using
|
||||
|
||||
```shell
|
||||
chmod +x IJP-ImageJ-Launcher-0.1.0-macosx-arm64
|
||||
chmod +x IJP-ImageJ-Launcher-0.1.0-macosx-arm64.command
|
||||
```
|
||||
|
||||
**8. Start ImageJ**
|
||||
|
||||
You can start `IJP-ImageL-Launcher-0.1.0-macosx-arm64` from command line or 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.
|
||||
|
|
|
|||
27
build.sbt
27
build.sbt
|
|
@ -1,6 +1,6 @@
|
|||
scalaVersion := "3.3.0"
|
||||
scalaVersion := "3.3.0"
|
||||
//name := "IJP-ImageJ-Launcher"
|
||||
version := "0.1.0"
|
||||
version := "0.1.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.1",
|
||||
"org.scalatest" %%% "scalatest" % "3.2.16" % 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"
|
||||
5
notes/v.0.2.0.md
Normal file
5
notes/v.0.2.0.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### New Features
|
||||
|
||||
* 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
|
||||
|
|
@ -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.9.0
|
||||
|
|
|
|||
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.11.0")
|
||||
|
|
@ -1 +1,2 @@
|
|||
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12")
|
||||
// https://github.com/scala-native/scala-native
|
||||
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14")
|
||||
|
|
|
|||
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