diff --git a/.scalafmt.conf b/.scalafmt.conf index c3aee56..580c3a5 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -11,4 +11,6 @@ docstrings.wrap = no importSelectors = singleLine newlines.source = keep -rewrite.scala3.convertToNewSyntax = yes \ No newline at end of file +rewrite.scala3.convertToNewSyntax = yes +rewrite.scala3.removeOptionalBraces = yes +rewrite.scala3.insertEndMarkerMinLines = 42 diff --git a/build.sbt b/build.sbt index 30a2749..b73a3a7 100644 --- a/build.sbt +++ b/build.sbt @@ -13,7 +13,7 @@ libraryDependencies ++= Seq( 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 => diff --git a/src/main/resources/scala-native/mem.c b/src/main/resources/scala-native/mem.c new file mode 100644 index 0000000..cd818e7 --- /dev/null +++ b/src/main/resources/scala-native/mem.c @@ -0,0 +1,19 @@ +#if defined(_WIN32) +#include +#else +#include +#endif + +unsigned long long determineTotalSystemMemory() +{ +#if defined(_WIN32) + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + GlobalMemoryStatusEx(&status); + return status.ullTotalPhys; +#else + long pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + return pages * page_size; +#endif +} diff --git a/src/main/scala/ij_plugins/imagej_launcher/ErrorCode.scala b/src/main/scala/ij_plugins/imagej_launcher/ErrorCode.scala deleted file mode 100644 index 16b20a9..0000000 --- a/src/main/scala/ij_plugins/imagej_launcher/ErrorCode.scala +++ /dev/null @@ -1,17 +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 scala.collection.immutable - -enum ErrorCode(val value: Int, val message: String): - case OK extends ErrorCode(0, "OK") - case InvalidCommandLineArguments extends ErrorCode(-10, "Invalid command line arguments") - case GeneralError extends ErrorCode(-100, "General error") - case UnhandledNonFatalError extends ErrorCode(-200, "Unhandled non-fatal error") - case UnhandledFatalError extends ErrorCode(-300, "Unhandled fatal error") - case NotImplemented extends ErrorCode(-400, "Functionality not implemented") - - override def toString: String = s"$message [exit code: $value]" diff --git a/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala b/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala index 7486d8b..a440600 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Launcher.scala @@ -9,37 +9,34 @@ import ij_plugins.imagej_launcher.Launcher.javaExeFileName import ij_plugins.imagej_launcher.Main.Config import os.Path -import java.io.{BufferedReader, File, InputStreamReader} +import java.io.File import java.lang.ProcessBuilder.Redirect -import java.util.Locale -class Launcher(logger: Logger) { +class Launcher(logger: Logger): private val jarsDirName = "jars" - def run(config: Config): ErrorCode = { + def run(config: Config): Unit = createCommandLine(config) match case Right(commandLine) => if config.dryRun then logger.debug("dry-run") println(commandLine.mkString(" ")) - ErrorCode.OK else launch(commandLine) case Left(errorMessage) => logger.error(errorMessage) - ErrorCode.GeneralError - } private def createCommandLine(config: Config): Either[String, Seq[String]] = - for { + for ijDir <- locateIJDir(config) launcherJar <- findImageJLauncherJar(ijDir) javaExe <- locateJavaExecutable(config, ijDir) systemType <- determineSystemType() - } yield { - buildCommandLine(ijDir, javaExe, launcherJar, systemType) - } + 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") @@ -54,22 +51,32 @@ class Launcher(logger: Logger) { dir.listFiles().find(f => f.getName == jarsDirName && f.isDirectory) match case Some(_) => - logger.debug(s" ImageJ directory set to: $dir") + 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 launch(command: Seq[String]): ErrorCode = - logger.debug("launchImageJ ...") - - logger.debug(s"Executing command:\n" + command.mkString(" ")) - - val builder = new ProcessBuilder(command*) - builder.redirectOutput(ProcessBuilder.Redirect.INHERIT) - builder.redirectError(ProcessBuilder.Redirect.INHERIT) - - val process = builder.start() - ErrorCode.OK + private def findImageJLauncherJar(ijDir: File): Either[String, File] = + logger.debug("Looking for 'imagej-launcher*.jar'") + val jarsDir = new File(ijDir, jarsDirName) + if jarsDir.exists() then + if jarsDir.isDirectory then + // Locate launcher + jarsDir + .listFiles() + .find { f => + val name = f.getName + name.startsWith("imagej-launcher") && name.endsWith(".jar") + } match + case Some(f) => + logger.info(s" Found launcher jar: '$f'") + Right(f) + case None => + Left(s"Cannot find 'imagej-launcher*.jar' in '$jarsDir'") + else + Left(s"'$jarsDir' is not a directory'") + else + Left(s"Cannot find subdirectory '$jarsDirName' [$jarsDir]") private def locateJavaExecutable(config: Config, ijDir: File): Either[String, File] = logger.debug("Looking for Java executable") @@ -92,11 +99,11 @@ class Launcher(logger: Logger) { Left("Unable to determine Java home") javaHomeE.flatMap(javaHome => - logger.debug(s" Java home set as: $javaHome") + logger.info(s" Java home set to: '$javaHome'") logger.debug(s" Looking for java executable bin/$javaExeFileName") val javaExe = new File(javaHome, s"bin/$javaExeFileName") if javaExe.exists() then - logger.debug(s" Found '$javaExeFileName' in '$javaHome/bin'") + logger.info(s" Found '$javaExeFileName' in: '$javaHome/bin'") Right(javaExe) else Left("cannot find 'java.exe' in 'path'") @@ -117,51 +124,44 @@ class Launcher(logger: Logger) { else None - private def findImageJLauncherJar(ijDir: File): Either[String, File] = - logger.debug("Looking for 'imagej-launcher*.jar'") - val jarsDir = new File(ijDir, jarsDirName) - if jarsDir.exists() then - if jarsDir.isDirectory then - // Locate launcher - jarsDir - .listFiles() - .find { f => - val name = f.getName - name.startsWith("imagej-launcher") && name.endsWith(".jar") - } match - case Some(f) => - logger.debug(s"Found: '$f'") - Right(f) - case None => - Left(s"Cannot find 'imagej-launcher*.jar' in '$jarsDir'") - else - Left(s"'$jarsDir' is not a directory'") - else - Left(s"Cannot find subdirectory '$jarsDirName' [$jarsDir]") - - private def determineSystemType(): Either[String, String] = { + private def determineSystemType(): Either[String, String] = + logger.debug("Determining system type") val osName = System.getProperty("os.name") val osArch = System.getProperty("os.arch") - logger.debug("os.name: " + osName) - logger.debug("os.arch: " + osArch) + logger.debug(" os.name: " + osName) + logger.debug(" os.arch: " + osArch) val notSupportedError = s"$osName $osArch not supported" - if osName.toLowerCase.startsWith("windows") then - if osArch.toLowerCase.contains("64") then - Right("win64") + val r = + if osName.toLowerCase.startsWith("windows") then + if osArch.toLowerCase.contains("64") then + Right("win64") + else + Left(notSupportedError) + else if osName.toLowerCase.contains("mac os x") then + Right("macosx") else Left(notSupportedError) - else if osName.toLowerCase.contains("mac os x") then - if osArch.toLowerCase.contains("aarch64") then - Right("aarch64") - else - Left(notSupportedError) - else - Left(notSupportedError) - } - private def buildCommandLine(ijDir: File, javaExe: File, launcherJar: File, systemType: String): Seq[String] = { + r.foreach(s => logger.info(s" System type set to: '$s'")) + r + + private def determineMaxMemoryMB(): Long = + logger.debug("Determine memory to use") + val sysMax = Native.mem.determineTotalSystemMemory() + logger.debug(s" Available RAM: ${sysMax / 1024 / 1024}MB") + val ijMaxMemMB = Math.round(sysMax * 0.75 / 1024 / 1024) + logger.debug(s" Using 3/4 of that: ${ijMaxMemMB}MB") + ijMaxMemMB + + private def buildCommandLine( + ijDir: File, + javaExe: File, + launcherJar: File, + systemType: String, + maxMemoryMB: Long + ): Seq[String] = val ijDirPath = ijDir.getAbsolutePath Seq( @@ -172,9 +172,11 @@ class Launcher(logger: Logger) { "java.base/java.util=ALL-UNNAMED", "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", + "--add-opens", + "java.desktop/com.apple.eawt=ALL-UNNAMED", "-Dpython.cachedir.skip=true", s"-Dplugins.dir=$ijDirPath", - "-Xmx2048m", + s"-Xmx${maxMemoryMB}m", "-Dimagej.splash=true", s"-Djava.class.path=${launcherJar.getAbsolutePath}", s"-Dimagej.dir=$ijDirPath", @@ -192,8 +194,17 @@ class Launcher(logger: Logger) { "plugins", "net.imagej.Main" ) - } -} + + private def launch(command: Seq[String]): Unit = + logger.debug("launchImageJ ...") + logger.debug(s" Executing command:\n" + command.mkString(" ")) + + val builder = new ProcessBuilder(command*) + builder.redirectOutput(ProcessBuilder.Redirect.INHERIT) + builder.redirectError(ProcessBuilder.Redirect.INHERIT) + + builder.start() +end Launcher object Launcher: lazy val javaExeFileName: String = if Utils.isWindows then "java.exe" else "java" diff --git a/src/main/scala/ij_plugins/imagej_launcher/Logger.scala b/src/main/scala/ij_plugins/imagej_launcher/Logger.scala index 3d8c94b..743169e 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Logger.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Logger.scala @@ -7,22 +7,22 @@ package ij_plugins.imagej_launcher import ij_plugins.imagej_launcher.Logger.Level -class Logger { +class Logger: var 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 = if l.level <= level.level then println(s"${l.name}: $message") -} + + def error(msg: String): Unit = pprint(Level.Error, msg) object Logger: enum Level(val level: Int, val name: String): - case Off extends Level(0, "OFF") + case Off extends Level(0, "OFF") case Error extends Level(200, "ERROR") - case Info extends Level(400, "INFO") + case Info extends Level(400, "INFO") case Debug extends Level(500, "DEBUG") - case All extends Level(Int.MaxValue, "DEBUG") + case All extends Level(Int.MaxValue, "DEBUG") diff --git a/src/main/scala/ij_plugins/imagej_launcher/Main.scala b/src/main/scala/ij_plugins/imagej_launcher/Main.scala index 722c71e..95038f1 100644 --- a/src/main/scala/ij_plugins/imagej_launcher/Main.scala +++ b/src/main/scala/ij_plugins/imagej_launcher/Main.scala @@ -5,12 +5,11 @@ package ij_plugins.imagej_launcher -import ij_plugins.imagej_launcher.ErrorCode import scopt.{DefaultOEffectSetup, OParser} import java.io.File -object Main { +object Main: private val logger = new Logger() private val AppName = "ijp-imagej-launcher" // private val AppVersion = s"${Version.version} [${Version.buildTimeStr}]" @@ -20,44 +19,18 @@ object Main { """Native launcher for ImageJ2 |""".stripMargin - case class Config( - logLevel: Logger.Level = Logger.Level.Error, - dryRun: Boolean = false, - javaHome: Option[File] = None, - ijDir: Option[File] = None - ) - def main(args: Array[String]): Unit = setupLogger(Logger.Level.All) - printInfo() - - val ret: ErrorCode = - parseCommandLine(args) match - case Some(config) => - setupLogger(config.logLevel) - runLauncher(config) - case None => - // arguments are bad - ErrorCode.InvalidCommandLineArguments - - val msg = s"${ret.message} [exit code: ${ret.value}]" - if ret == ErrorCode.OK then - logger.info(msg) - else - logger.error(msg) - -// System.exit(ret.value) - end main - - private def printInfo(): Unit = - logger.debug("os.arch: " + System.getProperty("os.arch")) - logger.debug("os.name: " + System.getProperty("os.name")) -// logger.debug("os.version: " + System.getProperty("os.version")) + parseCommandLine(args) match + case Some(config) => + setupLogger(config.logLevel) + runLauncher(config) + case None => private def parseCommandLine(args: Array[String]): Option[Config] = val builder = OParser.builder[Config] - val parser1 = { + val parser1 = import builder.* OParser.sequence( programName(AppName), @@ -94,12 +67,18 @@ object Main { .action((path, c) => c.copy(ijDir = Option(path))) .text("set the ImageJ directory to (used to find jars/, plugins/ and macros/)") ) - } OParser.parse(parser1, args, Config()) end parseCommandLine private def setupLogger(logLevel: Logger.Level): Unit = logger.level = logLevel - private def runLauncher(config: Config): ErrorCode = new Launcher(logger).run(config) -} + private def runLauncher(config: Config): Unit = new Launcher(logger).run(config) + + case class Config( + logLevel: Logger.Level = Logger.Level.Error, + dryRun: Boolean = false, + javaHome: Option[File] = None, + ijDir: Option[File] = None + ) +end Main diff --git a/src/main/scala/ij_plugins/imagej_launcher/Native.scala b/src/main/scala/ij_plugins/imagej_launcher/Native.scala new file mode 100644 index 0000000..2e847c4 --- /dev/null +++ b/src/main/scala/ij_plugins/imagej_launcher/Native.scala @@ -0,0 +1,9 @@ +package ij_plugins.imagej_launcher + +import scala.scalanative.unsafe.extern + +object Native: + + @extern + object mem: + def determineTotalSystemMemory(): Long = extern