Skip to content

Commit 387b841

Browse files
authored
Feature/cli installer (#390)
* feat: Improve headless install output - minimal and quiet - Fix NPE in ServiceLifecycleManager by initializing progressCallback - Suppress verbose output in headless mode (bundle loading, GitHub downloads, warnings, logging) - Redirect stdout/stderr to log file (~/.jdeploy/log/jdeploy-headless-install.log) - Only show essential messages: "Installing..." and "Installation complete" - Support headless mode in MainDebug for testing * feat: Add install and uninstall CLI commands to package.json Define commands for headless install/uninstall operations via CLI. * fix: Prevent GUI toolkit initialization in headless install mode Set java.awt.headless=true before any AWT classes are loaded to prevent the macOS menu bar from appearing during headless installs. The ImageIO usage in MacBundler was triggering AWT initialization. * fix: Skip dock/desktop/start menu items in headless install mode HeadlessInstallationSettings now disables addToDock, addToDesktop, addToStartMenu, and addToPrograms to prevent GUI interactions and screen flashes during CLI installs.
1 parent 1af6df0 commit 387b841

File tree

5 files changed

+229
-26
lines changed

5 files changed

+229
-26
lines changed

installer/package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"main": "index.js",
66
"preferGlobal": true,
77
"repository": {
8-
"type": "git",
98
"directory": "installer",
109
"url": "https://github.com/shannah/jdeploy.git"
1110
},
@@ -15,6 +14,7 @@
1514
"notarize": false,
1615
"packageLinuxX64": "jdeploy-installer-linux-x64",
1716
"packageMacArm64": "jdeploy-installer-mac-arm64",
17+
"javaVersion": "8",
1818
"packageLinuxArm64": "jdeploy-installer-linux-arm64",
1919
"downloadPage": {"platforms": ["all"]},
2020
"fork": false,
@@ -38,7 +38,19 @@
3838
"jar": "target/jdeploy-installer-1.0-SNAPSHOT.jar",
3939
"codesign": true,
4040
"platformBundlesEnabled": true,
41-
"packageWinArm64": "jdeploy-installer-win-arm64"
41+
"packageWinArm64": "jdeploy-installer-win-arm64",
42+
"commands": {
43+
"uninstall": {
44+
"implements": ["updater"],
45+
"args": ["uninstall"],
46+
"description": "CLI uninstaller"
47+
},
48+
"install": {
49+
"implements": ["updater"],
50+
"args": ["install"],
51+
"description": "CLI Install"
52+
}
53+
}
4254
},
4355
"dependencies": {"shelljs": "^0.8.4"},
4456
"license": "ISC",

installer/src/main/java/ca/weblite/jdeploy/installer/HeadlessInstallationSettings.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
import ca.weblite.jdeploy.installer.models.InstallationSettings;
44

5+
/**
6+
* Installation settings for headless (CLI) mode.
7+
* Disables GUI-related features like dock, desktop shortcuts, and start menu items.
8+
*/
59
public class HeadlessInstallationSettings extends InstallationSettings {
610

11+
public HeadlessInstallationSettings() {
12+
super();
13+
// Disable dock/desktop/start menu items in headless mode
14+
// These require GUI interaction or cause screen flashes
15+
setAddToDock(false);
16+
setAddToDesktop(false);
17+
setAddToStartMenu(false);
18+
setAddToPrograms(false);
19+
}
720
}

installer/src/main/java/ca/weblite/jdeploy/installer/Main.java

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ private Main() {
8989

9090
private Main(InstallationSettings settings) {
9191
this.installationSettings = settings;
92+
// Initialize the service lifecycle progress callback
93+
// Use SILENT by default - headless mode redirects output anyway,
94+
// and GUI mode will set up its own callback via the UI
95+
this.serviceLifecycleProgressCallback =
96+
ca.weblite.jdeploy.installer.services.ServiceLifecycleProgressCallback.SILENT;
9297
}
9398

9499
private Document getAppXMLDocument() throws IOException {
@@ -476,13 +481,20 @@ public static void main(String[] args) {
476481

477482
if (args.length == 1 && args[0].equals("install")) {
478483
headlessInstall = true;
484+
// Set AWT headless mode to prevent GUI toolkit initialization
485+
// This must be set before any AWT classes are loaded
486+
System.setProperty("java.awt.headless", "true");
479487
}
480488

481489
if (System.getProperty("jdeploy.background", "false").equals("true")) {
482490
runAsBackgroundHelper = true;
483491
}
484492

485-
if (!headlessInstall) {
493+
if (headlessInstall) {
494+
// For headless mode, set up output suppression immediately
495+
// This must happen before Main object creation which triggers verbose output
496+
setupStaticHeadlessOutputSuppression();
497+
} else {
486498
File logFile = new File(System.getProperty("user.home") + File.separator + ".jdeploy" + File.separator + "log" + File.separator + "jdeploy-installer.log");
487499
logFile.getParentFile().mkdirs();
488500
try {
@@ -500,6 +512,62 @@ public static void main(String[] args) {
500512
main.run();
501513
}
502514

515+
// Static fields for headless output suppression (set in main() before Main object creation)
516+
private static PrintStream staticOriginalOut;
517+
private static PrintStream staticOriginalErr;
518+
private static File staticLogFile;
519+
private static PrintStream staticLogStream;
520+
521+
/**
522+
* Allows external callers (like MainDebug) to set the original streams
523+
* before Main.main() is called. This is needed when the caller has already
524+
* set up output redirection and wants Main to use the true original streams.
525+
*/
526+
public static void setOriginalStreams(PrintStream out, PrintStream err, File logFile) {
527+
staticOriginalOut = out;
528+
staticOriginalErr = err;
529+
staticLogFile = logFile;
530+
}
531+
532+
/**
533+
* Sets up output suppression for headless mode at the static level.
534+
* Must be called before Main object creation to suppress verbose output from InstallationContext.
535+
*/
536+
private static void setupStaticHeadlessOutputSuppression() {
537+
// Only store streams if not already set by external caller (e.g., MainDebug)
538+
if (staticOriginalOut == null) {
539+
staticOriginalOut = System.out;
540+
}
541+
if (staticOriginalErr == null) {
542+
staticOriginalErr = System.err;
543+
}
544+
545+
// Suppress java.util.logging output
546+
java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("");
547+
java.util.logging.Handler[] handlers = rootLogger.getHandlers();
548+
for (java.util.logging.Handler handler : handlers) {
549+
handler.setLevel(java.util.logging.Level.OFF);
550+
}
551+
rootLogger.setLevel(java.util.logging.Level.OFF);
552+
553+
// Only set up log file redirection if not already done
554+
if (staticLogFile == null) {
555+
staticLogFile = new File(
556+
System.getProperty("user.home") + File.separator + ".jdeploy" +
557+
File.separator + "log" + File.separator + "jdeploy-headless-install.log"
558+
);
559+
}
560+
staticLogFile.getParentFile().mkdirs();
561+
562+
try {
563+
staticLogStream = new PrintStream(new FileOutputStream(staticLogFile));
564+
System.setOut(staticLogStream);
565+
System.setErr(staticLogStream);
566+
} catch (FileNotFoundException e) {
567+
staticOriginalErr.println("Warning: Could not create log file: " + e.getMessage());
568+
}
569+
}
570+
503571
private File findAppXmlFile() {
504572
return installationContext.findAppXml();
505573

@@ -510,19 +578,32 @@ public void run() {
510578
try {
511579
run0();
512580
} catch (Exception ex) {
513-
ex.printStackTrace(System.err);
514-
System.err.flush();
515-
invokeLater(()->{
516-
String message = ex.getMessage();
517-
if (ex instanceof UserLangRuntimeException) {
518-
UserLangRuntimeException userEx = (UserLangRuntimeException) ex;
519-
if (userEx.hasUserFriendlyMessage()) {
520-
message = userEx.getUserFriendlyMessage();
521-
}
522-
}
523-
uiFactory.showModalErrorDialog(null, message, "Installation failed.");
581+
// In headless mode, write to original stderr (not the redirected one)
582+
PrintStream errStream = staticOriginalErr != null ? staticOriginalErr : System.err;
583+
if (headlessInstall) {
584+
errStream.println("Installation failed: " + ex.getMessage());
585+
if (staticLogFile != null) {
586+
errStream.println("See log file for details: " + staticLogFile.getAbsolutePath());
587+
}
588+
// Also log full stack trace to log file
589+
ex.printStackTrace(System.err);
590+
System.err.flush();
524591
System.exit(1);
525-
});
592+
} else {
593+
ex.printStackTrace(System.err);
594+
System.err.flush();
595+
invokeLater(()->{
596+
String message = ex.getMessage();
597+
if (ex instanceof UserLangRuntimeException) {
598+
UserLangRuntimeException userEx = (UserLangRuntimeException) ex;
599+
if (userEx.hasUserFriendlyMessage()) {
600+
message = userEx.getUserFriendlyMessage();
601+
}
602+
}
603+
uiFactory.showModalErrorDialog(null, message, "Installation failed.");
604+
System.exit(1);
605+
});
606+
}
526607
}
527608
}
528609

@@ -861,6 +942,7 @@ private void onVisitSoftwareHomepage(InstallationFormEvent evt) {
861942
}
862943

863944
private void run0() throws Exception {
945+
// Output suppression for headless mode is handled in main() via setupStaticHeadlessOutputSuppression()
864946
loadAppInfo();
865947

866948
if (uninstall && Platform.getSystemPlatform().isWindows()) {
@@ -922,18 +1004,24 @@ private void runAsBackgroundHelperOnEdt() {
9221004
}
9231005

9241006
private void runHeadlessInstall() throws Exception {
925-
System.out.println(
926-
"jDeploy installer running in headless mode. Installing " +
927-
appInfo().getTitle() + " " + npmPackageVersion().getVersion()
1007+
// Output suppression is handled by setupStaticHeadlessOutputSuppression() in main()
1008+
// Use staticOriginalOut for user-facing messages
1009+
1010+
staticOriginalOut.println(
1011+
"Installing " + appInfo().getTitle() + " " + npmPackageVersion().getVersion() + "..."
9281012
);
1013+
9291014
try {
9301015
install();
9311016
} catch (Exception ex) {
932-
System.err.println("Installation failed");
933-
ex.printStackTrace(System.err);
934-
return;
1017+
staticOriginalErr.println("Installation failed: " + ex.getMessage());
1018+
if (staticLogFile != null) {
1019+
staticOriginalErr.println("See log file for details: " + staticLogFile.getAbsolutePath());
1020+
}
1021+
throw ex;
9351022
}
936-
System.out.println("Installation complete");
1023+
1024+
staticOriginalOut.println("Installation complete");
9371025
}
9381026

9391027
private File installedApp;

installer/src/main/java/ca/weblite/jdeploy/installer/MainDebug.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,47 @@
22

33
import org.apache.commons.io.FileUtils;
44
import java.io.File;
5+
import java.io.FileNotFoundException;
6+
import java.io.FileOutputStream;
7+
import java.io.PrintStream;
58

69
public class MainDebug {
7-
10+
11+
// Static fields for headless output suppression
12+
private static PrintStream originalOut;
13+
private static PrintStream originalErr;
14+
private static File logFile;
15+
private static PrintStream logStream;
16+
private static boolean headlessMode = false;
17+
818
public static void main(String[] args) throws Exception {
919
if (args.length < 1) {
10-
System.err.println("Usage: MainDebug <code> [version]");
20+
System.err.println("Usage: MainDebug <code> [version] [install]");
1121
System.err.println(" code: The jDeploy bundle code");
1222
System.err.println(" version: The version (defaults to 'latest' if not specified)");
23+
System.err.println(" install: Pass 'install' as third arg for headless mode");
1324
System.exit(1);
1425
}
1526

1627
String code = args[0];
1728
String version = args.length > 1 ? args[1] : "latest";
1829

30+
// Check if headless mode is requested (install argument in args)
31+
for (int i = 2; i < args.length; i++) {
32+
if ("install".equals(args[i])) {
33+
headlessMode = true;
34+
// Set AWT headless mode to prevent GUI toolkit initialization
35+
// This must be set before any AWT classes are loaded
36+
System.setProperty("java.awt.headless", "true");
37+
break;
38+
}
39+
}
40+
41+
// Set up output suppression for headless mode before any verbose operations
42+
if (headlessMode) {
43+
setupHeadlessOutputSuppression();
44+
}
45+
1946
File tempDir = File.createTempFile("jdeploy-debug2-", "");
2047
tempDir.delete();
2148
tempDir.mkdirs();
@@ -47,10 +74,53 @@ public static void main(String[] args) throws Exception {
4774
System.err.println("Failed to download .jdeploy-files for code: " + code + ", version: " + version);
4875
System.exit(1);
4976
}
50-
51-
Main.main(new String[0]);
77+
String[] newArgs;
78+
if (args.length > 2) {
79+
newArgs = new String[args.length - 2];
80+
System.arraycopy(args, 2, newArgs, 0, newArgs.length);
81+
} else {
82+
newArgs = new String[0];
83+
}
84+
// Pass original streams to Main so it can output user-facing messages
85+
if (headlessMode) {
86+
Main.setOriginalStreams(originalOut, originalErr, logFile);
87+
}
88+
Main.main(newArgs);
5289
} finally {
5390
System.setProperty("user.dir", originalDir.getAbsolutePath());
5491
}
5592
}
93+
94+
/**
95+
* Sets up output suppression for headless mode.
96+
* Redirects stdout and stderr to a log file and suppresses java.util.logging.
97+
*/
98+
private static void setupHeadlessOutputSuppression() {
99+
// Store original streams
100+
originalOut = System.out;
101+
originalErr = System.err;
102+
103+
// Suppress java.util.logging output
104+
java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("");
105+
java.util.logging.Handler[] handlers = rootLogger.getHandlers();
106+
for (java.util.logging.Handler handler : handlers) {
107+
handler.setLevel(java.util.logging.Level.OFF);
108+
}
109+
rootLogger.setLevel(java.util.logging.Level.OFF);
110+
111+
// Redirect stdout and stderr to log file
112+
logFile = new File(
113+
System.getProperty("user.home") + File.separator + ".jdeploy" +
114+
File.separator + "log" + File.separator + "jdeploy-headless-install.log"
115+
);
116+
logFile.getParentFile().mkdirs();
117+
118+
try {
119+
logStream = new PrintStream(new FileOutputStream(logFile));
120+
System.setOut(logStream);
121+
System.setErr(logStream);
122+
} catch (FileNotFoundException e) {
123+
originalErr.println("Warning: Could not create log file: " + e.getMessage());
124+
}
125+
}
56126
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package ca.weblite.jdeploy.installer.services;
2+
3+
/**
4+
* Console-based implementation of ServiceLifecycleProgressCallback for headless installations.
5+
* Prints progress messages to stdout in a clean format.
6+
*
7+
* @author Steve Hannah
8+
*/
9+
public class ConsoleServiceLifecycleProgressCallback implements ServiceLifecycleProgressCallback {
10+
11+
@Override
12+
public void updateProgress(String message) {
13+
System.out.println(message);
14+
}
15+
16+
@Override
17+
public void reportWarning(String message) {
18+
System.err.println("Warning: " + message);
19+
}
20+
}

0 commit comments

Comments
 (0)