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 0000000000..9df5ee29d9 Binary files /dev/null and b/src/standalone/assets/fireflyDockIcon.png differ diff --git a/src/standalone/assets/fireflySplash-old.png b/src/standalone/assets/fireflySplash-old.png new file mode 100644 index 0000000000..3281c98ca5 Binary files /dev/null and b/src/standalone/assets/fireflySplash-old.png differ diff --git a/src/standalone/assets/fireflySplash.png b/src/standalone/assets/fireflySplash.png new file mode 100644 index 0000000000..e5761e206e Binary files /dev/null and b/src/standalone/assets/fireflySplash.png differ diff --git a/src/standalone/assets/javaInstaller.sh b/src/standalone/assets/javaInstaller.sh new file mode 100755 index 0000000000..a113168870 --- /dev/null +++ b/src/standalone/assets/javaInstaller.sh @@ -0,0 +1,66 @@ +#!/bin/bash + + +getJreKey() { + arch=$(uname -m) + name=$(uname) + jarKey="unknown" + + if [[ "$name" == "Darwin" ]]; then + if [[ "$arch" == "arm64" ]]; then + jreKey="macOSArm64" + else + jreKey="macOSIntel" + fi + else + if [[ "$arch" == "aarch64" ]]; then + jreKey="linuxArm64" + elif [[ "$arch" == "x86_64" ]]; then + jreKey="linux64" + elif [[ "$arch" == *aarch* ]]; then + jreKey="linux64" + elif [[ "$arch" == *arm* ]]; then + jreKey="linux64" + elif [[ "$arch" == *amd64* ]]; then + jreKey="linux64" + elif [[ "$name" == "Linux" ]]; then + jreKey="linux64" + else + jreKey= + fi + fi + + echo "$jreKey" +} + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /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..6c35c5e770 --- /dev/null +++ b/src/standalone/assets/startFireflyServer.sh @@ -0,0 +1,260 @@ +#!/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="TRUE" +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" == "-f" || "$arg" == "--foreground" ]]; then + inBackground="FALSE" + 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 -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; +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 + +readyFile="$fireflyDir/ready-${fireflyPort}.txt" +/bin/rm -f "$readyFile" +{ + 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 "---------------------------------" + + 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/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..1445217383 --- /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.bundled) + 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..fa8938a88e --- /dev/null +++ b/src/standalone/java/edu/caltech/ipac/app/FireflyApplication.java @@ -0,0 +1,519 @@ +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 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 MenuItem aboutItem = 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(); + 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)); + + + if (useLogFile) setupLogger(); + + Tomcat tomcat = new Tomcat(); + tomcat.setBaseDir(tomcatDir.getAbsolutePath()); + tomcat.setPort(port); + + + boolean tomcatStarted = false; + if (!isRunning(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)..."); + Connector connector= tomcat.getConnector(); + connector.setPort(port); + connector.setProperty("compression", "on"); + connector.setProperty("useSendfile", "false"); + connector.setProperty("compressibleMimeType", compressibleMimeType); + tomcat.start(); + savePid(pidTextOutFile); + tomcatStarted = true; + } + + if (!tomcatStarted) { + terminalOut.println("Firefly is already running"); + openBrowser(port,true); + fireflyReadyMessage(port, null); + System.exit(0); + } + + + initComplete= true; + if (useDesktop) { + hideSplash(); + SwingUtilities.invokeLater(() -> updateAboutLabel(port)); + openBrowser(port,true); + } + fireflyReadyMessage(port, readyTextOutFile); + 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(File pidTextOutFile) { + 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, File pidTextOutFile, File readyTextOutFile) { + System.setProperty("apple.awt.UIElement", "true"); +// setupDock(port); + setupTray(tomcat, port, pidTextOutFile, readyTextOutFile); + 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, File pidTextOutFile, File readyTextOutFile) { + 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); + if (aboutItem!=null) { + aboutItem.setLabel(initComplete ? "About Firefly" : "About Firefly (initializing...)"); + } + } + + 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, File pidTextOutFile, File readyTextOutFile) { + 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"); + aboutItem = new MenuItem("About Firefly (initializing...)"); + 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, pidTextOutFile, readyTextOutFile)); + + try { + tray.add(trayIcon); + } catch (AWTException e) { + System.err.println("TrayIcon could not be added."); + } + } + + + 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) { + try { + FireflyApplication.start(); + } catch (Exception e) { + terminalOut.println("Error starting Firefly Application: " + e.getMessage()); + e.printStackTrace(); + } + Runtime.getRuntime().halt(0); + } +} +