From 9f9fb78162e31697fea015300e5a8bef5d02f53d Mon Sep 17 00:00:00 2001 From: roby Date: Tue, 19 May 2026 10:40:32 -0600 Subject: [PATCH 1/3] Firefly-1980: firefly standalone installation - includes response to feedback --- bin/get-firefly | 9 + bin/install.sh | 241 +++++++++ buildScript/dependencies.gradle | 2 + config/web.xml | 23 - docs/using-firefly-standalone.md | 172 ++++++ settings.gradle | 3 +- .../ipac/firefly/server/util/VersionUtil.java | 41 +- .../firefly/server/visualize/VisContext.java | 4 +- .../edu/caltech/ipac/util/StringUtils.java | 3 - .../ipac/util/download/URLDownload.java | 72 ++- .../visualize/plot/plotdata/FitsRead.java | 1 + src/standalone/assets/default_config.json | 7 + src/standalone/assets/ff | 129 +++++ src/standalone/assets/fireflyDockIcon.png | Bin 0 -> 7908 bytes src/standalone/assets/fireflySplash-old.png | Bin 0 -> 19748 bytes src/standalone/assets/fireflySplash.png | Bin 0 -> 26154 bytes src/standalone/assets/javaInstaller.sh | 66 +++ src/standalone/assets/jreVersion.json | 23 + src/standalone/assets/package-files.txt | 6 + src/standalone/assets/standalone_cleanup.sh | 47 ++ src/standalone/assets/startFireflyServer.sh | 247 +++++++++ src/standalone/assets/updater.sh | 19 + src/standalone/build.gradle | 76 +++ .../caltech/ipac/app/FireflyApplication.java | 511 ++++++++++++++++++ 24 files changed, 1630 insertions(+), 72 deletions(-) create mode 100644 bin/get-firefly create mode 100755 bin/install.sh create mode 100644 docs/using-firefly-standalone.md create mode 100644 src/standalone/assets/default_config.json create mode 100644 src/standalone/assets/ff create mode 100644 src/standalone/assets/fireflyDockIcon.png create mode 100644 src/standalone/assets/fireflySplash-old.png create mode 100644 src/standalone/assets/fireflySplash.png create mode 100755 src/standalone/assets/javaInstaller.sh create mode 100644 src/standalone/assets/jreVersion.json create mode 100644 src/standalone/assets/package-files.txt create mode 100644 src/standalone/assets/standalone_cleanup.sh create mode 100755 src/standalone/assets/startFireflyServer.sh create mode 100644 src/standalone/assets/updater.sh create mode 100644 src/standalone/build.gradle create mode 100644 src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java diff --git a/bin/get-firefly b/bin/get-firefly new file mode 100644 index 0000000000..0fde541c48 --- /dev/null +++ b/bin/get-firefly @@ -0,0 +1,9 @@ +#!/bin/bash +INSTALL_SCRIPT="https://raw.githubusercontent.com/Caltech-IPAC/firefly/refs/heads/dev/bin/install.sh" +#todo remove next line before PR merge +INSTALL_SCRIPT="https://raw.githubusercontent.com/Caltech-IPAC/firefly/refs/heads/FIREFLY-1980-standalone/bin/install.sh" + +curl -s ${INSTALL_SCRIPT} > ./install.sh +chmod +x ./install.sh +./install.sh -dontConfirm "$@" +/bin/rm -f ./install.sh \ No newline at end of file diff --git a/bin/install.sh b/bin/install.sh new file mode 100755 index 0000000000..369cf50b41 --- /dev/null +++ b/bin/install.sh @@ -0,0 +1,241 @@ +#!/bin/bash + +defaultInstallRelativePath="firefly" +INSTALL_DIR="$PWD/$defaultInstallRelativePath" +fireflyDir="${HOME}/.firefly" +applicationPath="current" +url= + +startScript="ff" +altUrl= +installJre="TRUE" +initialInstall="TRUE" +installType="installing" +doExit="FALSE" +doHelp="FALSE" +confirm="TRUE" +firstInvalid="TRUE" +space=" " + +echo "params: " $* + + +# -------------------------- +# define the isTrue function +# -------------------------- + +isTrue() { + v=$(echo "$1" | tr '[:upper:]' '[:lower:]') + if [[ "$v" == "true" || "$v" == "t" ]]; then return 0; else return 1; fi +} + +# -------------------------- +# get the parameters +# -------------------------- + +while [ $# -gt 0 ]; do + arg="$1" + if [[ "$arg" == "-url" ]]; then + shift + altUrl=$1 + elif [[ "$arg" == "-installDir" ]]; then + shift + enteredPath=$1 + mkdir -p "$enteredPath" + INSTALL_DIR=$(realpath "$1") + elif [[ "$arg" == "-asUpdate" ]]; then + installJre="FALSE" + initialInstall="FALSE" + applicationPath="new" + installType="updating" + elif [[ "$arg" == "-dontConfirm" ]]; then + confirm="FALSE" + elif [[ "$arg" == "--help" || "$arg" == "-h" ]]; then + doHelp="TRUE" + doExit="TRUE" + else + if isTrue $firstInvalid; then + echo "Invalid arguments passed." + firstInvalid="FALSE" + fi + echo "$space" "invalid argument:" "$arg" + doHelp="TRUE" + fi + shift +done + +# -------------------------- +# Help on the parameters +# -------------------------- + +if isTrue $doHelp; then + echo "Options:" + echo "$space -url: the url or the path to the firefly install zip" + echo "$space -installDir: the firefly install dir, defaults to ./firefly" + echo "$space -dontConfirm: the firefly install dir, defaults to ./firefly" + echo "$space -asUpdate: stage the install as an auto update" + echo "$space --help, -h: this message and exit" + exit 0; +fi + + +# -------------------------- +# validate and/or override install dir +# -------------------------- + +if isTrue $confirm && isTrue $initialInstall && [ "$enteredPath" == "" ]; then + read -p "Enter installation directory [${enteredPath:-$defaultInstallRelativePath}]: " enteredPath +fi + + +if [ "$INSTALL_DIR" != "" ]; then + mkdir -p "$INSTALL_DIR" +fi +if [[ ! -w $INSTALL_DIR ]]; then + echo "Cannot write to the installation dir ${INSTALL_DIR:-$enteredPath}" + exit 1 +fi + +# -------------------------- +# The install work begin here +# -------------------------- + +echo "$installType in $INSTALL_DIR" + +PACKAGE_ASSET_NAME="standalone.zip" +#todo remove next line before PR merge +PACKAGE_ASSET_NAME="test_standalone.zip" +applicationRoot="${INSTALL_DIR}/application" +applicationDir="${applicationRoot}/${applicationPath}" +binDir="${INSTALL_DIR}/bin" + +# -------------------------- +# make the directories, +# -------------------------- + +mkdir -p "$fireflyDir" +mkdir -p "$fireflyDir/server" +mkdir -p "$applicationDir" +mkdir -p "$binDir" +echo "$INSTALL_DIR" > "$fireflyDir/applicationPath.txt" +rm -f "$applicationDir"/complete + +# -------------------------- +# make the directories, +# -------------------------- +JQ=$(which jq) +if [[ "$JQ" == '' ]]; then + name=$(uname) + if [[ "$name" == "Darwin" ]]; then + echo jq is is missing from mac os, install failed + exit 1 + fi + arch=$(uname -m) + + if [[ "$arch" == "x86_64" ]]; then + jqUrl="https://github.com/jqlang/jq/releases/latest/download/jq-linux-amd64" + else + jqUrl="https://github.com/jqlang/jq/releases/latest/download/jq-linux-arm64" + fi + echo "installing local jq..." + curl -sL "$jqUrl" -o "$binDir/jq" + chmod +x "$binDir/jq" + JQ="$binDir/jq" +fi + + +# -------------------------- +# determine the default location of the standalone.jar package +# the default come from a github assert of the current firefly release +# -------------------------- + +targetPackageFile="${applicationDir}/standalone.zip" + +packageUrl=$(curl -s "https://api.github.com/repos/Caltech-IPAC/firefly/releases/latest" | \ +$JQ -r '.assets[] | [.name, .browser_download_url] | @tsv' | \ +while IFS=$'\t' read -r asset_name download_url; do + if [ "$asset_name" == $PACKAGE_ASSET_NAME ]; then + echo "$download_url" + fi +done) +if [ -z "$altUrl" ]; then + url=$packageUrl +else + url=$altUrl +fi + +if [ -z "$url" ]; then + echo "No package defined to download, could not find it as a github asset https://github.com/Caltech-IPAC/firefly/releases" + exit 0 +fi + + +# -------------------------- +# Download or copy the standalone.zip, expand, then expand firefly.war +# -------------------------- + +echo "install from: $url" +if [[ "$url" == http* ]]; then + curl -sL "$url" > "${targetPackageFile}" +else + cp "$url" "${targetPackageFile}" +fi +echo "expanding firefly $targetPackageFile..." +(cd "$applicationDir" && unzip -o "${targetPackageFile}" &> "${applicationDir}/standalone-expand.log") +mkdir -p "$applicationDir/firefly-war" +echo "expanding firefly.war..." +(cd "$applicationDir/firefly-war" && unzip -o "${applicationDir}/firefly.war" &> "${applicationDir}/war-expand.log") + +# -------------------------- +# make the script executable, put some in correct place +# -------------------------- + +scriptPath=$(realpath "$0") +cp "$scriptPath" "$applicationDir/install.sh" +chmod 775 "$applicationDir/standalone_cleanup.sh" \ + "$applicationDir/$startScript" \ + "$applicationDir/startFireflyServer.sh" \ + "$applicationDir/javaInstaller.sh" \ + "$applicationDir/updater.sh" \ + "$applicationDir/install.sh" +/bin/mv "$applicationDir/updater.sh" "$applicationRoot" + +cp "$applicationDir/$startScript" "$binDir" +chmod +x "$binDir/$startScript" + +# -------------------------- +# setup default port +# -------------------------- + + +if isTrue $initialInstall; then + /bin/cp "$applicationDir/default_config.json" "$fireflyDir/config.json" + if [ ! -f "$fireflyDir/user_ops.sh" ]; then + echo "JAVA_OPS=" > "$fireflyDir/user_ops.sh" + fi +fi + +# -------------------------- +# install java +# -------------------------- + +if isTrue $installJre; then + echo "installing java..." + JAVA=$("$applicationDir"/javaInstaller.sh) +fi + +# -------------------------- +# success message +# -------------------------- + +if isTrue $initialInstall; then + echo + echo "Firefly successfully installed, to start Firefly use the $startScript command" + echo + echo ">>>>>>>>>>>>>>>>>>>>>> ${binDir#$PWD/}/ff start" + echo + echo "You might want to add the bin dir to your PATH: $binDir" +fi + + +touch "$applicationDir"/complete \ No newline at end of file diff --git a/buildScript/dependencies.gradle b/buildScript/dependencies.gradle index 63b88a6061..c161bd450a 100644 --- a/buildScript/dependencies.gradle +++ b/buildScript/dependencies.gradle @@ -108,6 +108,8 @@ dependencies { implementation 'org.asdf-format:asdf-core:0.1-alpha-10' implementation 'edu.stsci:roman-datamodels:0.1-alpha-3' + // tomcat for standalone + } diff --git a/config/web.xml b/config/web.xml index 0bce306268..fec09b1874 100644 --- a/config/web.xml +++ b/config/web.xml @@ -89,29 +89,6 @@ /CmdSrv/async/* - - H2Console - org.h2.server.web.WebServlet - - -webAllowOthers - true - - 2 - - - H2Console - /admin/db/* - - - - alertviewer - /alertviewer.html - - - alertviewer - /alertviewer/* - - diff --git a/docs/using-firefly-standalone.md b/docs/using-firefly-standalone.md new file mode 100644 index 0000000000..4fa8f211de --- /dev/null +++ b/docs/using-firefly-standalone.md @@ -0,0 +1,172 @@ + + +# Installing a personal Firefly instance + +Firefly can be installed directly on your macOS or Linux desktop machine. +This is a full-featured installation that performs very well when working with local files. + + +## Installing Firefly + +### Quick install + +```bash +curl -L https://raw.githubusercontent.com/Caltech-IPAC/firefly/refs/heads/dev/bin/get-firefly | bash +``` +#### Usage +1. Change to the directory where you want to install Firefly. +1. Run the command above. +1. The installer will create a firefly directory containing the application and supporting files. + + +### Advanced Install + +#### 1. Download the installer + +```bash +curl -L https://raw.githubusercontent.com/Caltech-IPAC/firefly/refs/heads/dev/bin/install.sh -o install.sh +``` + +#### 2. Run the installer + +```bash +chmod +x install.sh +./install.sh +``` + +#### 3. Choose installation options + +The installer will prompt you for a destination directory. + +Use the following command to see all available options: + +```bash +./install.sh -h +``` + + +--- + +## Starting Firefly + +After installation, Firefly provides instructions for starting the server. + +The Firefly server is managed using the `firefly/bin/ff` script. + +To see all available commands and options: + +```bash +firefly/bin/ff --help +``` + +When Firefly starts, it automatically opens in your default web browser. + +The `ff` script uses commands as its primary argument. + +### Start Firefly + +```bash +firefly/bin/ff start +``` + +### Start Firefly in the Background + +```bash +firefly/bin/ff start --background +``` + +### Stop Firefly + +(Only needed when running in background mode.) + +```bash +firefly/bin/ff stop +``` + +### Tail Log Files + +```bash +firefly/bin/ff logs -f +``` + +### Check Server Status + +```bash +firefly/bin/ff status +``` + +### Uninstall Firefly + +```bash +firefly/bin/ff uninstall +``` + +--- + +## macOS UI Integration + +On macOS, Firefly creates a menu bar icon on the right side of the system menu bar. + +Use the drop-down menu to control and monitor the Firefly server. + +## Advanced Configuration + +Firefly can be configured using the JSON file: + +```text +~/.firefly/config.json +``` + +Edit this file to change the ports Firefly uses or to specify your own Java installation. + +Firefly requires Java 21 or later. By default, Firefly uses `"auto"` to automatically select a compatible Java runtime. + +If you want to use a Java installation already available on your system, replace the `java` entry in `config.json` with the path to your Java executable. + +### Default Configuration + +```json +{ + "ports": { + "firefly": 10233, + "redis": 10234 + }, + "java": "auto" +} +``` + +### Example Custom Configuration + +This example changes the Firefly port and uses a local Java installation: + +```json +{ + "ports": { + "firefly": 7777, + "redis": 102346 + }, + "java": "/usr/bin/java" +} +``` + +### Confirming firefly will run on your OS + +#### OSX +Firefly requires macOS 15 or greater + +#### Linux + +Firefly requires that `libssl.so.3` is on your linux system. +Check with the following command +```bash + /sbin/ldconfig -p | grep libssl.so.3 +``` +#### Linux version with `libssl.so.3` +- Debian 12 or later +- Red Hat 9 or later +- Ubuntu 22.04 LTS or later +- Fedora all recent releases + +#### Windows +Standalone Firefly is not supported on Windows + diff --git a/settings.gradle b/settings.gradle index ccc464db07..d0a80321cf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,8 @@ rootProject.name = 'firefly_root' -include 'firefly', 'firefly_data' +include 'firefly', 'firefly_data', "standalone" project(":firefly").projectDir = file('src/firefly') project(":firefly_data").projectDir = file('src/firefly_data') +project(":standalone").projectDir = file('src/standalone') diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/util/VersionUtil.java b/src/firefly/java/edu/caltech/ipac/firefly/server/util/VersionUtil.java index a340ecb394..718deb2eb4 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/util/VersionUtil.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/util/VersionUtil.java @@ -64,32 +64,35 @@ public static void initVersion(ServletContext context) { } public static void ingestVersion(ServletContext context) { - - _version.setAppName(context.getServletContextName()); - _version.setConfigLastModTime(ServerContext.getConfigLastModTime()); - - File confDir = ServerContext.getWebappConfigDir(); - Properties props = new Properties(); try { + _version.setAppName(context.getServletContextName()); + _version.setConfigLastModTime(ServerContext.getConfigLastModTime()); + + File confDir = ServerContext.getWebappConfigDir(); + Properties props = new Properties(); props.load(new FileInputStream(new File(confDir, VERSION_FILE))); - _version.setMajor(getNum(props.getProperty(MAJOR))); - _version.setMinor(getNum(props.getProperty(MINOR))); - _version.setRev(props.getProperty(REV)); - _version.setVersionType(Version.convertVersionType(props.getProperty(TYPE))); - _version.setBuild(getNum(props.getProperty(BUILD_NUMBER))); - _version.setBuildDate(props.getProperty(BUILD_DATE)); - _version.setBuildTime(props.getProperty(BUILD_TIME)); - _version.setBuildTag(props.getProperty(BUILD_TAG)); - _version.setBuildCommit(props.getProperty(BUILD_COMMIT)); - _version.setBuildCommitFirefly(props.getProperty(BUILD_COMMIT_FIREFLY)); - _version.setBuildFireflyTag(props.getProperty(BUILD_FIREFLY_TAG)); - _version.setBuildFireflyBranch(props.getProperty(BUILD_FIREFLY_BRANCH)); - _version.setDevCycleTag(props.getProperty(DEV_CYCLE_TAG)); + ingestVersion(props); } catch (IOException e) { // just ignore } } + public static void ingestVersion(Properties props) { + _version.setMajor(getNum(props.getProperty(MAJOR))); + _version.setMinor(getNum(props.getProperty(MINOR))); + _version.setRev(props.getProperty(REV)); + _version.setVersionType(Version.convertVersionType(props.getProperty(TYPE))); + _version.setBuild(getNum(props.getProperty(BUILD_NUMBER))); + _version.setBuildDate(props.getProperty(BUILD_DATE)); + _version.setBuildTime(props.getProperty(BUILD_TIME)); + _version.setBuildTag(props.getProperty(BUILD_TAG)); + _version.setBuildCommit(props.getProperty(BUILD_COMMIT)); + _version.setBuildCommitFirefly(props.getProperty(BUILD_COMMIT_FIREFLY)); + _version.setBuildFireflyTag(props.getProperty(BUILD_FIREFLY_TAG)); + _version.setBuildFireflyBranch(props.getProperty(BUILD_FIREFLY_BRANCH)); + _version.setDevCycleTag(props.getProperty(DEV_CYCLE_TAG)); + } + public static Version getAppVersion() { return _version; } private static int getNum(String s) { diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisContext.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisContext.java index 7f2bdc7ee8..a63cbd0ede 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisContext.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisContext.java @@ -27,7 +27,9 @@ public class VisContext { static public void init() { if (_initialized) return; - System.setProperty("java.awt.headless", "true"); +// System.setProperty("java.awt.headless", "true"); + boolean desktop= AppProperties.getBooleanProperty("runAsDesktopApplication", false); + System.setProperty("java.awt.headless", desktop ? "false" : "true"); //todo make this smart depending on context, no PR until it is done!!!! initFootprints(); initCounters(); _initialized = true; diff --git a/src/firefly/java/edu/caltech/ipac/util/StringUtils.java b/src/firefly/java/edu/caltech/ipac/util/StringUtils.java index 4584c83640..bdfd01e4ac 100644 --- a/src/firefly/java/edu/caltech/ipac/util/StringUtils.java +++ b/src/firefly/java/edu/caltech/ipac/util/StringUtils.java @@ -4,7 +4,6 @@ package edu.caltech.ipac.util; import edu.caltech.ipac.firefly.core.Util; -import edu.caltech.ipac.firefly.server.util.Logger; import javax.validation.constraints.NotNull; import java.net.MalformedURLException; @@ -45,8 +44,6 @@ public static enum Align {LEFT, RIGHT, MIDDLE} public static final long GIG_HUNDREDTH= GIG / 100; public static final long K = 1024; - private static final Logger.LoggerImpl logger = Logger.getLogger(); - public static String[] groupMatch(String regex, String val) { return groupMatch(regex, val, 0); } diff --git a/src/firefly/java/edu/caltech/ipac/util/download/URLDownload.java b/src/firefly/java/edu/caltech/ipac/util/download/URLDownload.java index 123e6462ae..e21bfc0403 100644 --- a/src/firefly/java/edu/caltech/ipac/util/download/URLDownload.java +++ b/src/firefly/java/edu/caltech/ipac/util/download/URLDownload.java @@ -58,7 +58,6 @@ public class URLDownload { private static final int BUFFER_SIZE = FileUtil.BUFFER_SIZE; - private static final Logger.LoggerImpl _log = Logger.getLogger(); private static final int MAX_REDIRECT= 2; static { @@ -83,7 +82,7 @@ public void checkServerTrusted(X509Certificate[] arg0, String arg1) { } sc.init(null, trustAllCerts, null); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); } catch (KeyManagementException | NoSuchAlgorithmException e) { - _log.error(e); + loggerError(e); } } @@ -200,11 +199,13 @@ public static Map buildReqHeaders(URL url, Map r if (requestHeaders== null) requestHeaders= Collections.emptyMap(); Map h = new HashMap<>(requestHeaders); if (ops==null || ops.useCredentials) { - var inputs= new HttpServiceInput(url.toString()); - var credentials= inputs.getHeaders(); - if (credentials!=null && !credentials.isEmpty()) { - if (!credentials.keySet().stream().allMatch(h::containsKey)) h.putAll(credentials); - } + try { + var inputs= new HttpServiceInput(url.toString()); + var credentials= inputs.getHeaders(); + if (credentials!=null && !credentials.isEmpty()) { + if (!credentials.keySet().stream().allMatch(h::containsKey)) h.putAll(credentials); + } + } catch (NoClassDefFoundError ignore) { } } return h; } @@ -247,7 +248,11 @@ private static String sendHeadersToCompactStr(Map> sendHeade } } else { - workBuff.append(sanitizeHeader(se.getKey(), String.valueOf(se.getValue()))); + try { + workBuff.append(sanitizeHeader(se.getKey(), String.valueOf(se.getValue()))); + } catch (NoClassDefFoundError e) { + workBuff.append(se.getKey()+"="+se.getValue()); + } } outStr.append(workBuff.toString()); } @@ -306,7 +311,7 @@ public static URLConnection makeConnection(URL url, } return conn; } catch (IOException e) { - logError(url,null,e); + loggerError(url,null,e); throw e; } } @@ -390,10 +395,10 @@ public static HttpResultInfo getDataFromURL(URL url, if (!ops.logErrorsOnly) logSuccess(result,url,dlSeconds,reqProp, postData); return result; } catch (SSLException | SocketTimeoutException | UnknownHostException e) { - logError(url, postData, e); + loggerError(url, postData, e); return exceptionToResponse(e,requestHeaders); } catch (IOException e) { - logError(url, postData, e); + loggerError(url, postData, e); throw new FailedRequestException(ResponseMessage.getNetworkCallFailureMessage(e), e, getResponseCode(conn)); } } @@ -418,7 +423,7 @@ public static HttpResultInfo getHeader(URL url, } catch (SSLException | SocketTimeoutException | UnknownHostException e) { return exceptionToResponse(e,h); } catch (IOException e) { - logError(url, null, e); + loggerError(url, null, e); throw new FailedRequestException(ResponseMessage.getNetworkCallFailureMessage(e), e, -1); } } @@ -476,10 +481,10 @@ private static HttpResultInfo getHeaderFromConnection(HttpURLConnection conn, } return result; } catch (SSLException | SocketTimeoutException | UnknownHostException e) { - logError(conn.getURL(), null , e); + loggerError(conn.getURL(), null , e); return exceptionToResponse(e,requestHeaders); } catch (IOException e) { - logError(conn.getURL(), null, e); + loggerError(conn.getURL(), null, e); throw new FailedRequestException(ResponseMessage.getNetworkCallFailureMessage(e), e, getResponseCode(conn)); } } @@ -505,7 +510,7 @@ public static FileInfo getDataToFileUsingPost(URL url, Map postData, Options ops= new Options(true, true, 0L, false, false, timeoutInSec, dl, false, false); return getDataToFile(makeURLConnection(url, cookies, requestHeader), outfile, ops, postData,0); } catch (IOException e) { - logError(url, postData, e); + loggerError(url, postData, e); throw new FailedRequestException(ResponseMessage.getNetworkCallFailureMessage(e), e); } } @@ -636,7 +641,7 @@ public static FileInfo getDataToFile(HttpURLConnection conn, } catch (SSLException | SocketTimeoutException | UnknownHostException e) { return exceptionToFileInfo(e); } catch (IOException e) { - logError(conn.getURL(), null, e); + loggerError(conn.getURL(), null, e); throw new FailedRequestException(ResponseMessage.getNetworkCallFailureMessage(e),e, getResponseCode(conn)); } } @@ -760,7 +765,7 @@ private static FileInfo checkAlreadyDownloaded(HttpURLConnection urlConn, File o urlConn.setIfModifiedSince(outfile.lastModified()); if (getResponseCode(urlConn) == HttpURLConnection.HTTP_NOT_MODIFIED) { String urlStr= urlConn.getURL().toString(); - _log.info(outfile.getName() + ": Not downloading, already have current version, from "+urlStr); + loggerInfo(outfile.getName() + ": Not downloading, already have current version, from "+urlStr); retval = new FileInfo(outfile, getSuggestedFileName(urlConn), HttpURLConnection.HTTP_NOT_MODIFIED, ResponseMessage.getHttpResponseMessage(HttpURLConnection.HTTP_NOT_MODIFIED)); retval.putAttribute(FileInfo.FILE_DOWNLOADED,false+""); @@ -774,7 +779,7 @@ private static FileInfo checkAlreadyDownloaded(HttpURLConnection urlConn, File o } - private static void logError(URL url, Map postData, Exception e) { + private static void loggerError(URL url, Map postData, Exception e) { List strList = new ArrayList<>(6); strList.add("----------Network Error-----------"); if (url != null) { @@ -788,7 +793,7 @@ private static void logError(URL url, Map postData, Exception e) { strList.add(StringUtils.pad(20,"----------Exception ")); strList.add(e.toString()); } - _log.warn(strList.toArray(new String[0])); + loggerInfo(strList.toArray(new String[0])); } private static void logHeaderForError(String originalUrl, Map postData, HttpURLConnection conn, Map> sendHeaders) { @@ -861,9 +866,9 @@ private static void logHeaderForError(String originalUrl, Map postData else { outStr.add("No headers or status received, invalid http response, using work around"); } - _log.info(outStr.toArray(new String[0])); + loggerInfo(outStr.toArray(new String[0])); } catch (Exception e) { - _log.info(e.getMessage() + ":" + " url=" + (conn.getURL()!=null ? conn.getURL().toString() : "none")); + loggerInfo(e.getMessage() + ":" + " url=" + (conn.getURL()!=null ? conn.getURL().toString() : "none")); } } @@ -880,7 +885,7 @@ private static void logSuccess(FileInfo fileInfo, File outfile, URL url, double "more response headers: "+otherHeadersToStr(fileInfo) )); if (postData!=null) strList.add("post data: " +postDataLogString(postData)); - _log.info(strList.toArray(new String[0])); + loggerInfo(strList.toArray(new String[0])); } private static void logSuccess(HttpResultInfo r, URL url, double dSeconds, Map> sendHeaders, Map postData) { @@ -889,7 +894,7 @@ private static void logSuccess(HttpResultInfo r, URL url, double dSeconds, Map values) { return workBuff.toString(); } + private static void loggerInfo(String ...msgs) { + try { + Logger.getLogger().info(msgs); + } catch (NoClassDefFoundError t) { + Arrays.stream(msgs).forEach(System.out::println); + } + } + + private static void loggerError(Throwable t, String ...msgs) { + try { + Logger.getLogger().error(t, msgs); + } catch (NoClassDefFoundError e) { + System.out.println(t.getMessage()); + Arrays.stream(msgs).forEach(System.out::println); + } + } + public static class Options { private boolean onlyIfModified; private boolean uncompress; diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java index 4904666054..b39cfb8bad 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java @@ -249,6 +249,7 @@ public static void setDefaultFutureStretch(RangeValues defaultRangeValues) { * @param lsstMasks mask array * @deprecated */ + @Deprecated public synchronized void doStretchMask( byte[] pixelData, int startPixel, diff --git a/src/standalone/assets/default_config.json b/src/standalone/assets/default_config.json new file mode 100644 index 0000000000..720b616dc4 --- /dev/null +++ b/src/standalone/assets/default_config.json @@ -0,0 +1,7 @@ +{ + "ports" : { + "firefly" : 10233, + "redis" : 10234 + }, + "java" : "auto" +} diff --git a/src/standalone/assets/ff b/src/standalone/assets/ff new file mode 100644 index 0000000000..db0f3f8cee --- /dev/null +++ b/src/standalone/assets/ff @@ -0,0 +1,129 @@ +#!/bin/bash + + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +testFile="$SCRIPT_DIR/../application/current/startFireflyServer.sh" +fireflyDir="${HOME}/.firefly" +logsDir="${fireflyDir}/server/logs" +pidFile="$fireflyDir/pid.txt" +space=" " + +if [ -x "$testFile" ]; then + INSTALL_DIR=$(realpath "$SCRIPT_DIR/..") +else + INSTALL_DIR=$(cat "$fireflyDir/applicationPath.txt") +fi + +applicationDir="${INSTALL_DIR}/application/current" + +isTrue() { + v=$(echo "$1" | tr '[:upper:]' '[:lower:]') + if [[ "$v" == "true" || "$v" == "t" ]]; then return 0; else return 1; fi +} + + +stop() { + if [[ "$1" == "help" || "$1" == "-h" || "$1" == "--help" ]]; then + echo "stop the firefly server" + else + if [[ -f "$pidFile" && -s "$pidFile" && -r "$pidFile" ]]; then + pid=$(cat "$pidFile"); + kill "$pid" + echo "Stopping firefly at process id $pid" + else + echo "Filefly is not running or pid file was lost" + fi + fi +} + +uninstall() { + if [[ "$1" == "help" || "$1" == "-h" || "$1" == "--help" ]]; then + echo "Uninstall the firefly server." + echo "The uninstall command will remove $fireflyDir and $INSTALL_DIR" + else + echo "uninstall will remove $fireflyDir and $INSTALL_DIR" + read -n1 -s -p "do you want to uninstall the firefly server? (y/n [n]): " doUninstall + echo + doUninstallLower=$(echo "$doUninstall" | tr '[:upper:]' '[:lower:]') + if [ "$doUninstallLower" = "y" ]; then + echo removing "$fireflyDir" + /bin/rm -rf "$fireflyDir" + echo removing "$INSTALL_DIR" + exec /bin/rm -rf "$INSTALL_DIR" + fi + fi +} + +logs() { + if [[ "$1" == "-f" || "$1" == "--follow" ]]; then + tail -f "$logsDir/application.log" "$logsDir/firefly.log" + elif [[ "$1" == "help" || "$1" == "-h" || "$1" == "--help" ]]; then + echo "show the log files" + echo "-f: tail the logs, -f will follow, like tail -f" + else + echo "------------------------------------------------------------" + echo "---------------------- application.log ---------------------" + echo "------------------------------------------------------------" + cat "$logsDir/application.log" + echo + echo + echo "------------------------------------------------------------" + echo "---------------------- firefly.log -------------------------" + echo "------------------------------------------------------------" + cat "$logsDir/firefly.log" + fi +} + +stat() { + if [[ "$1" == "help" || "$1" == "-h" || "$1" == "--help" ]]; then + echo "show if the firefly server is running and healthy" + else + up=$(curl -sD - -o /dev/null http://localhost:8888/firefly/healthz | head -1) + if [[ "$up" == *200* ]]; then + echo "The Firefly server is running and healthy" + elif [[ "$up" == "" ]]; then + echo "The Firefly server is not running" + else + echo "The Firefly server is return an error: $up" + fi + fi +} + + +help() { + echo + echo "-------- Help -------- " + echo "The ff command controls the firefly server and provides utilities" + echo "commands" + echo "$space start: start the firefly server" + echo "$space stop: stop the firefly server" + echo "$space logs: show the log files" + echo "$space status: show the server status" + echo "$space uninstall: uninstall the firstly server" + echo "$space help: show this message" + echo "you may follow a command with -h to get help on that command" + echo "example- ff start -h" + echo "example- ff logs -f" +} + +cmd=$1 +shift; +if [[ "$cmd" == "start" || "$cmd" == "s" ]]; then + exec "$applicationDir/startFireflyServer.sh" "$@" +elif [ "$cmd" == "stop" ]; then + stop "$@" +elif [[ "$cmd" == "logs" || "$cmd" == "log" || "$cmd" == "l" ]]; then + logs "$@" +elif [ "$cmd" == "status" ]; then + stat "$@" +elif [ "$cmd" == "uninstall" ]; then + uninstall "$@" +elif [[ "$cmd" == "help" || "$cmd" == "-h" || "$cmd" == "--help" ]]; then + help +elif [[ "$cmd" == "" ]]; then + help +else + echo "invalid command: $cmd" + help +fi + diff --git a/src/standalone/assets/fireflyDockIcon.png b/src/standalone/assets/fireflyDockIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..9df5ee29d946dcc97a5334a0c2b31955b63c268a GIT binary patch literal 7908 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

jn&`;^ygAZPnKF)yZy$^RliwPqMB+JuojPb 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 literal 0 HcmV?d00001 diff --git a/src/standalone/assets/fireflySplash.png b/src/standalone/assets/fireflySplash.png new file mode 100644 index 0000000000000000000000000000000000000000..e5761e206e44e8789e3b5a06380541079f7a4182 GIT binary patch literal 26154 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 /dev/null && pwd ) +INSTALL_DIR=$(cd "${SCRIPT_DIR}/../.." && pwd) +binDir="${INSTALL_DIR}/bin" +fireflyDir="${HOME}/.firefly" +javaInstallation="${INSTALL_DIR}/javaInstallation" +jreJsonFile="${INSTALL_DIR}/application/current/jreVersion.json" +configJsonFile="$fireflyDir/config.json" +JQ=$(which jq || echo "$binDir/jq") + + +jreKey=$(getJreKey) + +javaOverride=$($JQ -r ".java" "$configJsonFile") +if [[ $javaOverride != 'auto' && $javaOverride != "" && $javaOverride == *java ]]; then + JAVA=$javaOverride +else + jreUrl=$($JQ -r ".$jreKey.url" "$jreJsonFile") + javaPath=$($JQ -r ".$jreKey.java" "$jreJsonFile") + JAVA="$javaInstallation/$javaPath" +fi + +if [ -f "$JAVA" ]; then + echo $JAVA + exit 0 +fi + +mkdir "$javaInstallation" +curl -L "$jreUrl" > "$javaInstallation/jre.tar.gz" +(cd "$javaInstallation" && tar -xzvf jre.tar.gz &> $javaInstallation/jre_tar_expand.log) +echo $JAVA + diff --git a/src/standalone/assets/jreVersion.json b/src/standalone/assets/jreVersion.json new file mode 100644 index 0000000000..42b679aac7 --- /dev/null +++ b/src/standalone/assets/jreVersion.json @@ -0,0 +1,23 @@ +{ + "javaVersion" : "21.0.11", + "linux64" : { + "url": "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jre/hotspot/normal/eclipse", + "java" : "jdk-21.0.11+10-jre/bin/java" + }, + "linuxArm64" : { + "url": "https://api.adoptium.net/v3/binary/latest/21/ga/linux/aarch64/jre/hotspot/normal/eclipse", + "java" : "jdk-21.0.11+10-jre/bin/java" + }, + "macOSIntel" : { + "url": "https://api.adoptium.net/v3/binary/latest/21/ga/mac/x64/jre/hotspot/normal/eclipse", + "java" : "jdk-21.0.11+10-jre/Contents/Home/bin/java" + }, + "macOSArm64" : { + "url": "https://api.adoptium.net/v3/binary/latest/21/ga/mac/aarch64/jre/hotspot/normal/eclipse", + "java" : "jdk-21.0.11+10-jre/Contents/Home/bin/java" + }, + "window" : { + "url": "https://api.adoptium.net/v3/binary/latest/21/ga/windows/x64/jre/hotspot/normal/eclipse", + "java" : "don't know" + } +} \ No newline at end of file diff --git a/src/standalone/assets/package-files.txt b/src/standalone/assets/package-files.txt new file mode 100644 index 0000000000..5bc73bcdfc --- /dev/null +++ b/src/standalone/assets/package-files.txt @@ -0,0 +1,6 @@ + +firefly.war +tomcat-annotations-api-11.0.21.jar +tomcat-embed-core-11.0.21.jar +tomcat-embed-websocket-11.0.21.jar +standalone.jar diff --git a/src/standalone/assets/standalone_cleanup.sh b/src/standalone/assets/standalone_cleanup.sh new file mode 100644 index 0000000000..0735bf2b85 --- /dev/null +++ b/src/standalone/assets/standalone_cleanup.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +cleanupMinutes=1440 +logfileMaxAge=7200 +rm_cmd=/bin/rm +rm_cmd="echo will remove:" +workarea_dir="${HOME}/.firefly/server/workarea/firefly" +shared_workarea="${HOME}/.firefly/server/shared-workarea" +find_cmd="/usr/bin/find" + +doCleanup() { + workarea="${1}" + log_dir="${workarea}/cleanup_logs" + mkdir -p "${log_dir}" + + # cleanup old log files + ${find_cmd} "${log_dir}" -type f -mmin +${logfileMaxAge} -exec $rm_cmd '{}' \+ + + # cleanup old work files + timestamp=$(date +20%y%m%dT%H%M%S) + log_file="${log_dir}/cleanup.${timestamp}.log" + clean_dirs=("${workarea}/temp_files" "${workarea}/visualize/fits-cache" "${workarea}/visualize/users") + dirs_to_clear=("${workarea}/visualize/users" "${workarea}/temp_files") + echo "Cleanup: " $workarea + echo 'Cleanup: log file: ' "${log_file}" + { + echo "Cleaning up work files older that ${cleanupMinutes} minutes, dir: ${workarea}" + [[ -d "${workarea}/HiPS" ]] && ${find_cmd} "${workarea}/HiPS" -type f -mtime +90 -exec $rm_cmd '{}' \+ -print + [[ -d "${workarea}/stage" ]] && ${find_cmd} "${workarea}/stage" -type f -mtime +7 -exec $rm_cmd '{}' \+ -print + [[ -d "${workarea}/upload" ]] && ${find_cmd} "${workarea}/upload" -type f -mtime +7 -exec $rm_cmd '{}' \+ -print + [[ -d "${workarea}/perm_files" ]] && ${find_cmd} "${workarea}/perm_files" -type f -atime +1 -exec $rm_cmd '{}' \+ -print + for dir in "${clean_dirs[@]}"; do + if [ -d "${dir}" ]; then + ${find_cmd} "${dir}" -type f -amin +${cleanupMinutes} -exec $rm_cmd '{}' \+ -print + fi + done + for dir in "${dirs_to_clear[@]}"; do # remove empty directories excluding those at the starting level + [[ -d "${dir}" ]] && ${find_cmd} "${dir}" -mindepth 1 -depth -type d -empty -print -exec $rm_cmd '{}' \; + done + } > "${log_file}" 2>&1 +} + + +# Remove temporary products for each Firefly workarea +# Find app directories (should be only one, but loop for safety) +doCleanup "${workarea_dir}" + diff --git a/src/standalone/assets/startFireflyServer.sh b/src/standalone/assets/startFireflyServer.sh new file mode 100755 index 0000000000..702a4a74d2 --- /dev/null +++ b/src/standalone/assets/startFireflyServer.sh @@ -0,0 +1,247 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +INSTALL_DIR=$(cd "${SCRIPT_DIR}/../.." && pwd) +fireflyDir="${HOME}/.firefly" +fireflyServer="${HOME}/.firefly/server" +applicationDir="${INSTALL_DIR}/application/current" +appNew="${INSTALL_DIR}/application/new" +applicationJars="${applicationDir}/jars" +appLog="${fireflyServer}/logs/application.log" +userOpsFile="${fireflyDir}/user_ops.sh" +configJsonFile="$fireflyDir/config.json" +ADMIN_USER="admin" +ADMIN_PASSWORD="admin" +MIN_JVM_SIZE=1G +MAX_JVM_SIZE=10G +binDir="${INSTALL_DIR}/bin" +JQ=$(which jq || echo "$binDir/jq") + +# todo - i think we can remove serverConfigDir +serverConfigDir="${HOME}/config" + +isTrue() { + v=$(echo "$1" | tr '[:upper:]' '[:lower:]') + if [[ "$v" == "true" || "$v" == "t" ]]; then return 0; else return 1; fi +} + + +getFireflyStatusOnPort() { + curl --max-time 12 -sD - -o /dev/null http://localhost:$1/firefly/healthz > /tmp/fireflyStatusCheck.txt + curlStat=$? + if [ $curlStat -eq 28 ]; then + echo "INUSE" + return; + else + up=$(head -1 /tmp/fireflyStatusCheck.txt) + fi + + if [[ "$up" == *200* ]]; then + echo "UP" + elif [[ "$up" == "" ]]; then + echo "FREE" + else + echo "INUSE" + fi +} + +debugParams= +loggingLevel="INFO" +doClean="FALSE" +verbose="FALSE" +doExit="FALSE" +doHelp="FALSE" +firstInvalid="TRUE" +inBackground="FALSE" +overridePort="" +alreadyRunning="FALSE" +space=" " + +while [ $# -gt 0 ]; do + arg="$1" + if [[ "$arg" == "--debug" || "$arg" == "-debug" ]]; then + debugParams="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" + elif [ "$arg" == "--verbose" ]; then + verbose="TRUE" + loggingLevel="DEBUG" + elif [ "$arg" == "--clean" ]; then + doClean="TRUE" + elif [ "$arg" == "--cleanAndExit" ]; then + doClean="TRUE" + doExit="TRUE" + elif [[ "$arg" == "-d" || "$arg" == "--background" ]]; then + inBackground="TRUE" + elif [[ "$arg" == "--port" ]]; then + shift + overridePort=$1 + elif [[ "$arg" == "--help" || "$arg" == "-h" ]]; then + doHelp="TRUE" + doExit="TRUE" + else + if isTrue $firstInvalid; then + echo "Invalid arguments passed." + firstInvalid="FALSE" + fi + echo "$space" "invalid argument:" "$arg" + doHelp="TRUE" + fi + shift +done + +if isTrue $doHelp; then + echo "Options:" + echo "$space --debug, -debug: start and pause in java debug mode on port 5005" + echo "$space --verbose: more startup logging and set java log level to debug" + echo "$space --clean: clean work area before startup" + echo "$space --cleanAndExit: clean work only and exit" + echo "$space -d, --background: start in background" + echo "$space --port: a port number to override the default firefly port, it can also be set in ~/.firefly/config.json" + echo "$space --help, -h: this message and exit" + exit 0; +fi + +if isTrue $doClean; then + /bin/rm -rf "${fireflyServer}/workarea" + echo "removing: ${fireflyServer}/workarea" + /bin/rm -rf "${fireflyServer}/temp" + echo "removing: ${fireflyServer}/temp" + /bin/rm -rf "${fireflyServer}/logs" + echo "removing: ${fireflyServer}/logs" + /bin/rm -rf "${fireflyServer}/work" + echo "removing: ${fireflyServer}/work" + if isTrue $doExit; then + exit 0; + fi + fi + +if [[ -d "$appNew" && -f "$appNew/complete" ]]; then + if [[ -f "$INSTALL_DIR/disableUpdate" ]]; then + echo ">>>>>>>>> Update available but disabled" + else + echo ">>>>>>>>> updating..." + exec "$appNew/../updater.sh" + fi +fi + +if [ ! -f "$applicationDir/jars/firefly.jar" ]; then + FILES_FROM_WAR="WEB-INF/lib/firefly.jar WEB-INF/lib/json-simple-1.1.1.jar WEB-INF/config/version.tag" + (cd $applicationDir && unzip -oj firefly.war ${FILES_FROM_WAR} ) +fi + +redisPort=$($JQ -r ".ports.redis" "$configJsonFile") + +if [[ $overridePort == "" ]]; then + fireflyPort=$($JQ -r ".ports.firefly" "$configJsonFile") +else + fireflyPort=$overridePort +fi + +ffStat=$(getFireflyStatusOnPort "$fireflyPort") +if [[ $ffStat == "UP" ]]; then + alreadyRunning="TRUE" +elif [[ $ffStat == "FREE" ]]; then + alreadyRunning="FALSE" +elif [[ $ffStat == "INUSE" ]]; then + echo "The port number $fireflyPort is being used by another application" + echo "You can change the port my editing ~/.firefly/config.json or by using the --port parameter" + exit 1; +fi + + +[ ! -d "${applicationJars}" ] && mkdir "$applicationJars" +[ ! -d "${fireflyServer}/temp" ] && mkdir "${fireflyServer}/temp" +[ ! -d "${fireflyServer}/logs" ] && mkdir "${fireflyServer}/logs" +if isTrue $verbose; then + echo "$applicationDir/*.jar" to "$applicationJars" +fi +if ls $applicationDir/*.jar >/dev/null 2>&1; then + /bin/mv $applicationDir/*.jar "$applicationJars" +fi + + + +JAVA_OPS= +if [ -f "$userOpsFile" ]; then + source $userOpsFile + if isTrue $verbose; then + echo JAVA_OPS = $JAVA_OPS + fi +fi + + +name=$(uname) +if [[ "$name" == "Darwin" ]]; then + splash="-splash:${applicationDir}/fireflySplash.png" + nameParam='-Xdock:name=Firefly Server' + runAsDesktopApplication="true" + headless="false" + #dockIcon="-Xdock:icon=${applicationDir}/fireflyDockIcon.png" --- keep around if we decide to read dock +else + splash= + nameParam="-DdockPlaceHolder=" + runAsDesktopApplication="false" + headless="true" + #dockIcon="" +fi + +PROPS=" \ + -Dapple.awt.UIElement=true \ + -Xms${MIN_JVM_SIZE} -Xmx${MAX_JVM_SIZE} ${debugParams} \ + --add-opens java.base/java.util=ALL-UNNAMED \ + -XX:+UnlockExperimentalVMOptions \ + -XX:TrimNativeHeapInterval=30000 \ + -XX:+UseZGC \ + -Dnet.sf.ehcache.enableShutdownHook=true \ + -Dlogging.level=${loggingLevel} \ + -Djava.net.preferIPv4Stack=true \ + -Dwork.directory=${fireflyServer}/workarea \ + -DrunAsDesktopApplication=${runAsDesktopApplication} \ + -Djava.awt.headless=${headless} \ + -Dvisualize.fits.search.path=${HOME} \ + -Dredis.db.dir=${fireflyServer}/temp/redis \ + -Djava.io.tmpdir=${fireflyServer}/temp \ + -Dalerts.dir=${fireflyServer}/alerts \ + -Dserver_config_dir=${serverConfigDir} \ + -Dfirefly.port=${fireflyPort} + -Dredis.port=${redisPort:-6379} \ + -DADMIN_USER=${ADMIN_USER} \ + -DADMIN_PASSWORD=${ADMIN_PASSWORD} \ + -DADMIN_PROTECTED= \ + -DuserHelpToLog=${inBackground} \ + ${JAVA_OPS} + " + + +JAVA=$("$applicationDir"/javaInstaller.sh) + +export CLASSPATH=${applicationJars}'/*' +if isTrue $verbose; then + echo + echo Using classpath + echo $CLASSPATH + echo +fi + +{ + echo "------------------------------------------------" + echo "---------- Starting Firefly server" + echo "---------- $(date)" + echo "------------------------------------------------" + echo ${JAVA} ${splash} "${nameParam}" ${PROPS} edu.caltech.ipac.app.FireflyApplication +} >> "$appLog" + +if isTrue $inBackground; then + (cd "$applicationDir" && ${JAVA} ${splash} "${nameParam}" ${PROPS} edu.caltech.ipac.app.FireflyApplication &> "${fireflyServer}/logs/backgroundStart.log" &) + if isTrue $alreadyRunning; then + echo "Firefly is already running on port" + else + echo "Firefly server starting in background (it takes a few seconds)..." + fi + echo + echo "---------------------------------" + echo "Firefly URL: http://localhost:$fireflyPort/firefly/" + echo "---------------------------------" +else + (cd "$applicationDir" && ${JAVA} ${splash} "${nameParam}" ${PROPS} edu.caltech.ipac.app.FireflyApplication ) +fi + diff --git a/src/standalone/assets/updater.sh b/src/standalone/assets/updater.sh new file mode 100644 index 0000000000..4bf9539dc4 --- /dev/null +++ b/src/standalone/assets/updater.sh @@ -0,0 +1,19 @@ +#!/bin/bash + + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +INSTALL_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +applicationRoot="${INSTALL_DIR}/application" +appNew="${applicationRoot}/new" +appCurrent="${applicationRoot}/current" +appOld="${applicationRoot}/old" + + +if [[ -d "$appNew" && -d "$appCurrent" && -f "$appNew/complete" ]]; then + /bin/rm -rf "$appOld" + /bin/mv "$appCurrent" "$appOld" + /bin/mv "$appNew" "$appCurrent" +fi + +exec "$appCurrent/startFireflyServer.sh" + diff --git a/src/standalone/build.gradle b/src/standalone/build.gradle new file mode 100644 index 0000000000..7c006352db --- /dev/null +++ b/src/standalone/build.gradle @@ -0,0 +1,76 @@ +group 'edu.caltech.ipac' + +ext["app-name"] = 'standalone' + +configurations { + bundled +} + +sourceSets { + main.java.srcDir '.' + main.resources { + srcDirs "." + exclude "**/*.gradle" + } +} + +dependencies { + implementation ':firefly' + implementation 'org.apache.tomcat.embed:tomcat-embed-core:11.0.21' + implementation 'org.apache.tomcat.embed:tomcat-embed-websocket:11.0.21' + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:11.0.21' + bundled 'org.apache.tomcat.embed:tomcat-embed-core:11.0.21' + bundled 'org.apache.tomcat.embed:tomcat-embed-websocket:11.0.21' + bundled 'org.apache.tomcat.embed:tomcat-embed-jasper:11.0.21' +} + +// sourceSets { +// main.java { +// srcDir '**/firefly_standalone/**' +// } +// } + + +jar { + archiveFileName = 'firefly_standalone.jar' + include "**/*" + from sourceSets.main.allJava +} + + +def stageTarget= file("${buildDir}/zipFiles") +def fireflyWar= project(':firefly').tasks.named('war') +def fireflyData= project(':firefly_data').tasks.named('jar') +def fireflyWarFile= file("$rootDir/build/dist/firefly.war") + +compileJava.dependsOn fireflyWar + +task stageFiles(type: Copy) { + dependsOn jar + into stageTarget +// from { fireflyWar.get().archivePath } // show firefly-1.0.war + from { fireflyWarFile } + from { jar.archivePath } + from configurations.bundled + + from("${rootDir}/src/standalone/assets") + } + +task zip(type: Jar, dependsOn: stageFiles) { + archiveFileName = "standalone.zip" + from "${buildDir}/zipFiles" + destinationDirectory = file ("$rootDir/build/dist") +} + +task copyStandaloneDependencies(dependsOn: jar) { + doLast { + def homePath = System.properties['user.home'] + def publishDir = project.group.replaceAll("\\.", "/") + "/${project.name}/${project.version}" + def repoDir = file("${homePath}/.m2/repository/" + publishDir) + copy { + from(configurations.runtimeClasspath) + into repoDir + } + println "Firefly local maven directory: ${repoDir}" + } +} diff --git a/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java b/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java new file mode 100644 index 0000000000..e35bf58389 --- /dev/null +++ b/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java @@ -0,0 +1,511 @@ +package edu.caltech.ipac.app; + +import edu.caltech.ipac.firefly.server.util.VersionUtil; +import edu.caltech.ipac.util.AppProperties; +import edu.caltech.ipac.util.FileUtil; +import edu.caltech.ipac.util.StringUtils; +import edu.caltech.ipac.util.download.FailedRequestException; +import edu.caltech.ipac.util.download.URLDownload; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import javax.swing.*; +import java.awt.AWTException; +import java.awt.BorderLayout; +import java.awt.Desktop; +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SplashScreen; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.MalformedURLException; +import java.net.ServerSocket; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Properties; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + + +public class FireflyApplication { + + private static final File pwd = new File(System.getProperty("user.dir")); + private static final File ffDir = new File(System.getProperty("user.home"), ".firefly"); + private static final File installDir = new File(pwd, "../..").getAbsoluteFile(); + private static final File tomcatDir = new File(ffDir,"server"); + private static final File tomcatTmp = new File(tomcatDir, "temp"); + private static final File tomcatLogs = new File(tomcatDir, "logs"); + private static final File applicationRoot = new File(installDir, "application"); + private static final File applicationDir = new File(applicationRoot, "current"); + private static final File cleanupScript= new File(applicationDir,"standalone_cleanup.sh"); + private static final File installScript= new File(applicationDir,"install.sh"); + private static final File configFile = new File(ffDir, "config.json"); + private static final File dockIconFile= new File(applicationDir,"fireflyDockIcon.png"); + private static final File applicationLogFile= new File(tomcatLogs, "application.log"); + private static final File fireflyWarDir= new File(applicationDir, "firefly-war"); + private static final File versionTagPropFile= new File(applicationDir, "version.tag"); + private static final File versionTextOutFile= new File(ffDir, "version.txt"); + private static final File pidTextOutFile= new File(ffDir, "pid.txt"); + private static final String compressibleMimeType= String.join(",", Arrays.asList( + "text/html", "text/plain", "text/css", "text/javascript", + "application/javascript", "application/json", "application/xml", + "text/xml", "application/x-votable+xml", "application/x-yaml", "application/ld+json", + "image/svg+xml", "text/csv", "application/xhtml+xml", + "application/rss+xml", "application/atom+xml", "application/x-font-ttf", + "font/otf", "font/woff", "font/woff2", + "application/octet-stream" + )); + private static final boolean useLogFile= true; + private static final int DEFAULT_PORT= 8888; + private static String fireflyVersion; + private static String javaVersion; + private static boolean updateAvailable= false; + private static PrintStream terminalOut= System.out; + private static boolean initComplete= false; + private static JLabel aboutLabel= null; // only used in desktop mode + private static boolean firstUpdateCheck= true; + + + + public static void start() throws LifecycleException, URISyntaxException, IOException, InterruptedException { + fireflyVersion= saveVersion(); + javaVersion = System.getProperty("java.version"); + boolean useDesktop= AppProperties.getBooleanProperty("runAsDesktopApplication", false); + ensureFireflyDir(); + var port= getPort(); + + if (useDesktop) SwingUtilities.invokeLater(() -> initAboutLabel(port)); + + + if (useLogFile) setupLogger(); + + Tomcat tomcat = new Tomcat(); + tomcat.setBaseDir(tomcatDir.getAbsolutePath()); + tomcat.setPort(port); + + + boolean tomcatStarted = false; + if (!isRunning(port)) { + if (useDesktop) setupUI(tomcat,port); + tomcat.addUser("admin", "admin"); + tomcat.addWebapp("/firefly", fireflyWarDir.getAbsolutePath()); + terminalOut.println("Firefly server starting (is takes a few seconds)..."); + Connector connector= tomcat.getConnector(); + connector.setPort(port); + connector.setProperty("compression", "on"); + connector.setProperty("useSendfile", "false"); + connector.setProperty("compressibleMimeType", compressibleMimeType); + tomcat.start(); + savePid(); + tomcatStarted = true; + } + + if (!tomcatStarted) { + terminalOut.println("Firefly is already running"); + openBrowser(port,true); + fireflyReadyMessage(port); + System.exit(0); + } + + + initComplete= true; + if (useDesktop) { + hideSplash(); + SwingUtilities.invokeLater(() -> updateAboutLabel(port)); + openBrowser(port,true); + } + fireflyReadyMessage(port); + Thread.sleep(5 * 1000); // 5 seconds + updateAvailable= doAutoUpdateCheck(); + doWorkAreaCleanup(); + while (tomcat.getServer().getState().isAvailable()) { + Thread.sleep(3600 * 1000); // 1 hour + if (!updateAvailable) updateAvailable= doAutoUpdateCheck(); + doWorkAreaCleanup(); + } + + } + + public static boolean doAutoUpdateCheck() { + boolean updateAvailable= false; + try { + var result= URLDownload.getDataFromURL(new URI("https://api.github.com/repos/Caltech-IPAC/firefly/releases/latest").toURL(),null,null); + var obj= (JSONObject) new JSONParser().parse(result.getResultAsString()); + var availableVersion= (String) obj.get("name"); + var newVerAvailable= isNewVersionAvailable(fireflyVersion,availableVersion); + + String urlStr= null; + var assetsAry= (JSONArray)obj.get("assets"); + if (newVerAvailable && assetsAry!=null && !assetsAry.isEmpty()) { + for(Object entry: assetsAry){ + JSONObject asset= (JSONObject)entry; + if (StringUtils.areEqual((String)asset.get("name"),"standalone.zip")) { + urlStr= (String)asset.get("url"); + } + } + } + String overrideUrlStr= null; +// overrideUrlStr= "/Users/roby/dev/firefly/build/dist/standalone.zip"; // todo remove after testing + if (overrideUrlStr!=null) urlStr= overrideUrlStr; + updateAvailable= urlStr!=null; + if (updateAvailable) doUpdateInstall(urlStr); + + String updateMsg= updateAvailable ? ", Update available (relaunch Firefly to finish update)" : ""; + + String msg= "**** Update Check: Current Version: "+fireflyVersion + + ", Available version: "+ availableVersion + + ", Java Version: "+ javaVersion + updateMsg; + + if (firstUpdateCheck) terminalOut.println(msg); + System.out.println(msg); + firstUpdateCheck= false; + + } catch (FailedRequestException | MalformedURLException | URISyntaxException | ParseException e) { + System.out.println(e.toString()); + } + return updateAvailable; + } + + public static boolean isNewVersionAvailable(String currVer, String availableVer) { + if (currVer==null) currVer= "0,0.0"; + if (availableVer==null) availableVer= "0,0.0"; + var cVer= currVer.split("\\."); + var nVer= availableVer.split("\\."); + if (cVer.length!=3 || nVer.length!=3) return false; + var curr= Arrays.stream(cVer).map((s) -> StringUtils.getInt(s,0)).toList(); + var next= Arrays.stream(nVer).map((s) -> StringUtils.getInt(s,0)).toList(); + return (next.get(0)>curr.get(0) || next.get(1)>curr.get(1) || next.get(2)>curr.get(2)); + } + + + public static void savePid() { + FileUtil.writeStringToFile(pidTextOutFile,ProcessHandle.current().pid()+""); + } + + public static String saveVersion() { + try { + Properties props = new Properties(); + props.load(new FileInputStream(versionTagPropFile)); + VersionUtil.ingestVersion(props); + var vInfo= VersionUtil.getVersionInfo(); + var fVerList= vInfo.stream().filter(kv -> kv.getKey().equals("Firefly Version")).toList(); + if (fVerList.size()==1) { + var vStr= fVerList.getFirst().getValue(); + + String major="0"; + String minor="0"; + String rev="0"; + var phase1= vStr.split("-"); + var realVStr= phase1[0]; + var parts= realVStr.split("\\."); + if (parts.length>1) { + major= parts[0]; + minor= parts[1]; + if (parts.length>2) rev= parts[2]; + } + var version= major+"."+minor+"."+rev; + FileUtil.writeStringToFile(versionTextOutFile, version); + return version; + } + } catch (IOException e) { + System.out.println("failed to get version: " + e.toString()); + } + return null; + } + + public static void doUpdateInstall(String packageUrl) { + ProcessBuilder pb = new ProcessBuilder(installScript.getAbsolutePath(), + "-url", packageUrl, "-asUpdate", + "-installDir", installDir.getAbsolutePath() ); + try { + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + int exitCode = process.waitFor(); + if (exitCode != 0) System.out.println("auto update job failed with code: " + exitCode); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + public static void setupLogger() throws IOException { + Logger logger= Logger.getLogger(""); + for (Handler h : logger.getHandlers()) { + logger.removeHandler(h); + } + + + // setup logger + Handler fileHandler = new FileHandler(applicationLogFile.getAbsolutePath(), true); + fileHandler.setFormatter(new SimpleFormatter()); + fileHandler.setLevel(Level.ALL); + logger.addHandler(fileHandler); + + // set system out for stuff that logger misses + System.setOut(new PrintStream(new FileOutputStream(applicationLogFile,true))); + + + String terminalDevice = System.getProperty("os.name").toLowerCase().contains("win") + ? "CON" : "/dev/tty"; + boolean helpToLog= AppProperties.getBooleanProperty("userHelpToLog", false); + terminalOut = helpToLog ? System.out : new PrintStream(new FileOutputStream(terminalDevice)); + } + + public static void doWorkAreaCleanup() { + ProcessBuilder pb = new ProcessBuilder(cleanupScript.getAbsolutePath()," --once"); + try { + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + int exitCode = process.waitFor(); + if (exitCode != 0) System.out.println("clean up job failed with code: " + exitCode); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + + public static void openBrowser(int port, boolean doSleep) { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + try { + if (doSleep) Thread.sleep(500); + Desktop.getDesktop().browse(new URI(makeUrlString(port))); + } catch (URISyntaxException | InterruptedException | IOException ignore) { + System.out.println("Could not open browser"); + } + } + } + + public static String makeUrlString(int port) { return "http://localhost:"+port+"/firefly/";} + + public static void ensureFireflyDir() { + confirmDirOrExit(ffDir); + confirmDirOrExit(tomcatDir); + confirmDirOrExit(tomcatTmp); + confirmDirOrExit(tomcatLogs); + } + + public static int getPort() { + int portProp= AppProperties.getIntProperty("firefly.port",0); + if (portProp!=0) return portProp; + try { + if (!configFile.canRead()) return DEFAULT_PORT; + String pStr= FileUtil.readFile(configFile); + if (pStr==null) return DEFAULT_PORT; + var obj= (JSONObject) new JSONParser().parse(pStr); + var ports= (JSONObject)obj.get("ports"); + if (ports==null) return DEFAULT_PORT; + Long port= (Long)ports.get("firefly"); + if (port==null) return DEFAULT_PORT; + return port.intValue(); + } catch (IOException | NumberFormatException | ParseException e) { + return DEFAULT_PORT; + } + } + + private static void confirmDirOrExit(File dir) { + boolean exists = true; + if (!dir.exists()) { + exists = dir.mkdir(); + } + if (!exists || !dir.canWrite()) { + System.out.println("Can't write to " + dir.getAbsolutePath() + " directory"); + System.exit(0); + } + } + + private static void setupUI(Tomcat tomcat, int port) { + System.setProperty("apple.awt.UIElement", "true"); +// setupDock(port); + setupTray(tomcat, port); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + if (tomcat.getServer().getState().isAvailable()) { + System.out.println("Shutting down Firefly server..."); + tomcat.stop(); + tomcat.destroy(); + var ignore= pidTextOutFile.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + finally { + Runtime.getRuntime().halt(0); + } + })); + } + +// Keep this around- we might want to reenable the dock, todo - what does linux do with this code? +// public static void setupDock(int port) { +// //System.setProperty("apple.awt.UIElement", "false"); <<- this property should be set to false on the java command line +// System.setProperty("apple.laf.useScreenMenuBar", "true"); +// System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Firefly"); +// if (Desktop.isDesktopSupported()) { +// Desktop desktop = Desktop.getDesktop(); +// if (desktop.isSupported(Desktop.Action.APP_ABOUT)) { +// desktop.setAboutHandler(e -> showAboutDialog(port) ); +// } +// } +// +// } + + public static void stopFireflyServer(Tomcat tomcat) { + try { + if (tomcat.getServer().getState().isAvailable()) { + System.out.println("Shutting down Firefly server..."); + tomcat.stop(); + tomcat.destroy(); + pidTextOutFile.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + finally { + Runtime.getRuntime().halt(0); + } + } + + public static void initAboutLabel(int port) { + aboutLabel= new JLabel(); + aboutLabel.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + try { + Desktop.getDesktop().browse(new URI(makeUrlString(port))); + } catch (Exception ignore) { } + } + }); + } + + public static void updateAboutLabel(int port) { + if (aboutLabel==null) return; + String outstr= String.format("Firefly Version: %s
Java Version: %s
", + fireflyVersion, javaVersion); + outstr+= String.format("To load Firefly: %s",makeUrlString(port), makeUrlString(port)); + if (updateAvailable) outstr+= "

"+"Update available (relaunch Firefly to finish update)"; + if (!initComplete)outstr+= "

"+"Server Initializing..."; + aboutLabel.setText(outstr); + aboutLabel.setToolTipText(outstr); + } + + public static void showAboutDialog(int port, JFrame frame) { + if (aboutLabel==null) return; + SwingUtilities.invokeLater(() -> { + + updateAboutLabel(port); + + JDialog aboutDialog = new JDialog(frame, "About Firefly", true); + aboutDialog.setLayout(new BorderLayout()); + + aboutLabel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + aboutDialog.add(aboutLabel, BorderLayout.CENTER); + + aboutDialog.pack(); + aboutDialog.setSize(450, 150); + aboutDialog.setLocationRelativeTo(null); // Center on screen + aboutDialog.setAlwaysOnTop(true); + aboutDialog.setVisible(true); + }); + } + + + + public static boolean isRunning(int port) { + try (ServerSocket serverSocket = new ServerSocket(port)) { + return false; // Port is available + } catch (IOException e) { + return true; // Port is in use + } + } + + public static void hideSplash() { + SplashScreen splash = SplashScreen.getSplashScreen(); + if (splash != null) splash.close(); + } + + public static void setupTray(Tomcat tomcat, int port) { + System.setProperty("apple.awt.enableTemplateImages", "false"); + if (!SystemTray.isSupported()) { + System.out.println("SystemTray is not supported on this platform."); + return; + } + SystemTray tray = SystemTray.getSystemTray(); + Image image = Toolkit.getDefaultToolkit().getImage(dockIconFile.getAbsolutePath()); + + var dummyAnchor = new JFrame(); + dummyAnchor.setType(Window.Type.UTILITY); + dummyAnchor.setUndecorated(true); + dummyAnchor.setSize(1, 1); + dummyAnchor.setLocationRelativeTo(null); + + + // Create a popup menu for the icon + PopupMenu popup = new PopupMenu(); + MenuItem exitItem = new MenuItem("Shutdown Firefly Server"); + MenuItem aboutItem = new MenuItem("About Firefly"); + MenuItem openInBrowser = new MenuItem("Open in Browser: " + makeUrlString(port)); + popup.add(openInBrowser); + popup.add(aboutItem); + popup.addSeparator(); + popup.add(exitItem); + aboutItem.addActionListener(e -> showAboutDialog(port, dummyAnchor) ); + TrayIcon trayIcon = new TrayIcon(image, "Firefly Server", popup); + trayIcon.setImageAutoSize(true); // Automatically scale the image + openInBrowser.addActionListener(e -> openBrowser(port, false)); + exitItem.addActionListener(e -> stopFireflyServer(tomcat)); + + try { + tray.add(trayIcon); + } catch (AWTException e) { + System.err.println("TrayIcon could not be added."); + } + } + + + public static void fireflyReadyMessage(int port) throws IOException { + terminalOut.println("\n---------------------------------"); + terminalOut.println("Firefly ready: use URL: " + makeUrlString(port)); + terminalOut.println("---------------------------------\n"); + } + + public static void main(String[] args) { + try { + FireflyApplication.start(); + } catch (Exception e) { + terminalOut.println("Error starting Firefly Application: " + e.getMessage()); + e.printStackTrace(); + } + Runtime.getRuntime().halt(0); + } +} + From eab18de9075ef9873acfb01f8f8ef6eed64908d0 Mon Sep 17 00:00:00 2001 From: roby Date: Thu, 28 May 2026 13:25:19 -0600 Subject: [PATCH 2/3] mroe cleanup --- src/standalone/assets/startFireflyServer.sh | 23 ++++++++++--- .../caltech/ipac/app/FireflyApplication.java | 34 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/standalone/assets/startFireflyServer.sh b/src/standalone/assets/startFireflyServer.sh index 702a4a74d2..6c35c5e770 100755 --- a/src/standalone/assets/startFireflyServer.sh +++ b/src/standalone/assets/startFireflyServer.sh @@ -52,7 +52,7 @@ verbose="FALSE" doExit="FALSE" doHelp="FALSE" firstInvalid="TRUE" -inBackground="FALSE" +inBackground="TRUE" overridePort="" alreadyRunning="FALSE" space=" " @@ -69,8 +69,8 @@ while [ $# -gt 0 ]; do elif [ "$arg" == "--cleanAndExit" ]; then doClean="TRUE" doExit="TRUE" - elif [[ "$arg" == "-d" || "$arg" == "--background" ]]; then - inBackground="TRUE" + elif [[ "$arg" == "-f" || "$arg" == "--foreground" ]]; then + inBackground="FALSE" elif [[ "$arg" == "--port" ]]; then shift overridePort=$1 @@ -94,7 +94,7 @@ if isTrue $doHelp; then echo "$space --verbose: more startup logging and set java log level to debug" echo "$space --clean: clean work area before startup" echo "$space --cleanAndExit: clean work only and exit" - echo "$space -d, --background: start in background" + echo "$space -f, --foreground: start in foreground (start in background by default)" echo "$space --port: a port number to override the default firefly port, it can also be set in ~/.firefly/config.json" echo "$space --help, -h: this message and exit" exit 0; @@ -181,7 +181,7 @@ else nameParam="-DdockPlaceHolder=" runAsDesktopApplication="false" headless="true" - #dockIcon="" + #dockIcon= fi PROPS=" \ @@ -222,6 +222,8 @@ if isTrue $verbose; then echo fi +readyFile="$fireflyDir/ready-${fireflyPort}.txt" +/bin/rm -f "$readyFile" { echo "------------------------------------------------" echo "---------- Starting Firefly server" @@ -237,10 +239,21 @@ if isTrue $inBackground; then else echo "Firefly server starting in background (it takes a few seconds)..." fi + echo echo "---------------------------------" echo "Firefly URL: http://localhost:$fireflyPort/firefly/" echo "---------------------------------" + + if ! isTrue $alreadyRunning; then + echo -n "Firefly server waiting for init to complete..." + ready=$(cat "$readyFile" 2> /dev/null) + while ! isTrue $ready; do + sleep .5 + ready=$(cat "$readyFile" 2> /dev/null) + done + echo "Ready" + fi else (cd "$applicationDir" && ${JAVA} ${splash} "${nameParam}" ${PROPS} edu.caltech.ipac.app.FireflyApplication ) fi diff --git a/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java b/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java index e35bf58389..fa8938a88e 100644 --- a/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java +++ b/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java @@ -66,7 +66,6 @@ public class FireflyApplication { private static final File fireflyWarDir= new File(applicationDir, "firefly-war"); private static final File versionTagPropFile= new File(applicationDir, "version.tag"); private static final File versionTextOutFile= new File(ffDir, "version.txt"); - private static final File pidTextOutFile= new File(ffDir, "pid.txt"); private static final String compressibleMimeType= String.join(",", Arrays.asList( "text/html", "text/plain", "text/css", "text/javascript", "application/javascript", "application/json", "application/xml", @@ -84,6 +83,7 @@ public class FireflyApplication { private static PrintStream terminalOut= System.out; private static boolean initComplete= false; private static JLabel aboutLabel= null; // only used in desktop mode + private static MenuItem aboutItem = null; // only used in desktop mode private static boolean firstUpdateCheck= true; @@ -94,6 +94,9 @@ public static void start() throws LifecycleException, URISyntaxException, IOExce boolean useDesktop= AppProperties.getBooleanProperty("runAsDesktopApplication", false); ensureFireflyDir(); var port= getPort(); + File readyTextOutFile= new File(ffDir, "ready-"+port+".txt"); +// File pidTextOutFile= new File(ffDir, "pid-"+port+".txt"); + File pidTextOutFile= new File(ffDir, "pid.txt"); if (useDesktop) SwingUtilities.invokeLater(() -> initAboutLabel(port)); @@ -107,7 +110,8 @@ public static void start() throws LifecycleException, URISyntaxException, IOExce boolean tomcatStarted = false; if (!isRunning(port)) { - if (useDesktop) setupUI(tomcat,port); + var ignore= readyTextOutFile.delete(); + if (useDesktop) setupUI(tomcat, port, pidTextOutFile, readyTextOutFile); tomcat.addUser("admin", "admin"); tomcat.addWebapp("/firefly", fireflyWarDir.getAbsolutePath()); terminalOut.println("Firefly server starting (is takes a few seconds)..."); @@ -117,14 +121,14 @@ public static void start() throws LifecycleException, URISyntaxException, IOExce connector.setProperty("useSendfile", "false"); connector.setProperty("compressibleMimeType", compressibleMimeType); tomcat.start(); - savePid(); + savePid(pidTextOutFile); tomcatStarted = true; } if (!tomcatStarted) { terminalOut.println("Firefly is already running"); openBrowser(port,true); - fireflyReadyMessage(port); + fireflyReadyMessage(port, null); System.exit(0); } @@ -135,7 +139,7 @@ public static void start() throws LifecycleException, URISyntaxException, IOExce SwingUtilities.invokeLater(() -> updateAboutLabel(port)); openBrowser(port,true); } - fireflyReadyMessage(port); + fireflyReadyMessage(port, readyTextOutFile); Thread.sleep(5 * 1000); // 5 seconds updateAvailable= doAutoUpdateCheck(); doWorkAreaCleanup(); @@ -199,7 +203,7 @@ public static boolean isNewVersionAvailable(String currVer, String availableVer) } - public static void savePid() { + public static void savePid(File pidTextOutFile) { FileUtil.writeStringToFile(pidTextOutFile,ProcessHandle.current().pid()+""); } @@ -345,10 +349,10 @@ private static void confirmDirOrExit(File dir) { } } - private static void setupUI(Tomcat tomcat, int port) { + private static void setupUI(Tomcat tomcat, int port, File pidTextOutFile, File readyTextOutFile) { System.setProperty("apple.awt.UIElement", "true"); // setupDock(port); - setupTray(tomcat, port); + setupTray(tomcat, port, pidTextOutFile, readyTextOutFile); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { if (tomcat.getServer().getState().isAvailable()) { @@ -380,7 +384,7 @@ private static void setupUI(Tomcat tomcat, int port) { // // } - public static void stopFireflyServer(Tomcat tomcat) { + public static void stopFireflyServer(Tomcat tomcat, File pidTextOutFile, File readyTextOutFile) { try { if (tomcat.getServer().getState().isAvailable()) { System.out.println("Shutting down Firefly server..."); @@ -416,6 +420,9 @@ public static void updateAboutLabel(int port) { if (!initComplete)outstr+= "

"+"Server Initializing..."; aboutLabel.setText(outstr); aboutLabel.setToolTipText(outstr); + if (aboutItem!=null) { + aboutItem.setLabel(initComplete ? "About Firefly" : "About Firefly (initializing...)"); + } } public static void showAboutDialog(int port, JFrame frame) { @@ -453,7 +460,7 @@ public static void hideSplash() { if (splash != null) splash.close(); } - public static void setupTray(Tomcat tomcat, int port) { + public static void setupTray(Tomcat tomcat, int port, File pidTextOutFile, File readyTextOutFile) { System.setProperty("apple.awt.enableTemplateImages", "false"); if (!SystemTray.isSupported()) { System.out.println("SystemTray is not supported on this platform."); @@ -472,7 +479,7 @@ public static void setupTray(Tomcat tomcat, int port) { // Create a popup menu for the icon PopupMenu popup = new PopupMenu(); MenuItem exitItem = new MenuItem("Shutdown Firefly Server"); - MenuItem aboutItem = new MenuItem("About Firefly"); + aboutItem = new MenuItem("About Firefly (initializing...)"); MenuItem openInBrowser = new MenuItem("Open in Browser: " + makeUrlString(port)); popup.add(openInBrowser); popup.add(aboutItem); @@ -482,7 +489,7 @@ public static void setupTray(Tomcat tomcat, int port) { TrayIcon trayIcon = new TrayIcon(image, "Firefly Server", popup); trayIcon.setImageAutoSize(true); // Automatically scale the image openInBrowser.addActionListener(e -> openBrowser(port, false)); - exitItem.addActionListener(e -> stopFireflyServer(tomcat)); + exitItem.addActionListener(e -> stopFireflyServer(tomcat, pidTextOutFile, readyTextOutFile)); try { tray.add(trayIcon); @@ -492,10 +499,11 @@ public static void setupTray(Tomcat tomcat, int port) { } - public static void fireflyReadyMessage(int port) throws IOException { + public static void fireflyReadyMessage(int port, File readyTextOutFile) throws IOException { terminalOut.println("\n---------------------------------"); terminalOut.println("Firefly ready: use URL: " + makeUrlString(port)); terminalOut.println("---------------------------------\n"); + if (readyTextOutFile!=null) FileUtil.writeStringToFile(readyTextOutFile,"TRUE"); } public static void main(String[] args) { From 2499f2db9c0b32e1a114e86b0947074ce56a893b Mon Sep 17 00:00:00 2001 From: roby Date: Thu, 28 May 2026 15:57:26 -0600 Subject: [PATCH 3/3] clean up mavin copy --- src/standalone/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/standalone/build.gradle b/src/standalone/build.gradle index 7c006352db..1445217383 100644 --- a/src/standalone/build.gradle +++ b/src/standalone/build.gradle @@ -68,7 +68,7 @@ task copyStandaloneDependencies(dependsOn: jar) { def publishDir = project.group.replaceAll("\\.", "/") + "/${project.name}/${project.version}" def repoDir = file("${homePath}/.m2/repository/" + publishDir) copy { - from(configurations.runtimeClasspath) + from(configurations.bundled) into repoDir } println "Firefly local maven directory: ${repoDir}"