@@ -2423,7 +2423,7 @@
Options
$("#rawSpoolSize").val(1);
$("#rawEncoding").val(null);
$("#rawSpoolEnd").val(null);
- $("#rawAltPrinting").prop('checked', false);
+ $("#rawForceRaw").prop('checked', false);
$("#rawCopies").val(1);
//printer
@@ -3011,7 +3011,7 @@
Options
}
cfg.reconfigure({
- altPrinting: includedValue($("#rawAltPrinting"), isChecked($("#rawAltPrinting"), cleanConditions['rawAltPrinting'])),
+ forceRaw: includedValue($("#rawForceRaw"), isChecked($("#rawForceRaw"), cleanConditions['rawForceRaw'])),
encoding: includedValue($("#rawEncoding")),
spool: { size: spoolSize, end: includedValue($("#rawSpoolEnd")) },
diff --git a/src/org/dyorgio/jna/platform/mac/Foundation.java b/src/org/dyorgio/jna/platform/mac/Foundation.java
index af097ba81..2bb143a61 100644
--- a/src/org/dyorgio/jna/platform/mac/Foundation.java
+++ b/src/org/dyorgio/jna/platform/mac/Foundation.java
@@ -35,7 +35,7 @@
*/
public interface Foundation extends Library {
- public static final Foundation INSTANCE = Native.load("Foundation", Foundation.class);
+ Foundation INSTANCE = Native.load("Foundation", Foundation.class);
NativeLong class_getInstanceVariable(NativeLong classPointer, String name);
diff --git a/src/org/dyorgio/jna/platform/mac/NSApplication.java b/src/org/dyorgio/jna/platform/mac/NSApplication.java
new file mode 100644
index 000000000..2326cf189
--- /dev/null
+++ b/src/org/dyorgio/jna/platform/mac/NSApplication.java
@@ -0,0 +1,22 @@
+package org.dyorgio.jna.platform.mac;
+
+import com.sun.jna.NativeLong;
+import com.sun.jna.Pointer;
+
+public class NSApplication extends NSObject {
+ static NativeLong klass = Objc.INSTANCE.objc_lookUpClass("NSApplication");
+ static Pointer sharedApplication = Objc.INSTANCE.sel_getUid("sharedApplication");
+ static Pointer activateIgnoringOtherApps = Objc.INSTANCE.sel_getUid("activateIgnoringOtherApps:");
+
+ public static NSApplication sharedApplication() {
+ return new NSApplication(Objc.INSTANCE.objc_msgSend(klass, sharedApplication));
+ }
+
+ public NSApplication(NativeLong handle) {
+ super(handle);
+ }
+
+ public void activateIgnoringOtherApps(boolean flag) {
+ Objc.INSTANCE.objc_msgSend(this.getId(), activateIgnoringOtherApps, flag);
+ }
+}
\ No newline at end of file
diff --git a/src/org/dyorgio/jna/platform/mac/Objc.java b/src/org/dyorgio/jna/platform/mac/Objc.java
new file mode 100644
index 000000000..b8f0d3122
--- /dev/null
+++ b/src/org/dyorgio/jna/platform/mac/Objc.java
@@ -0,0 +1,12 @@
+package org.dyorgio.jna.platform.mac;
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.NativeLong;
+import com.sun.jna.Pointer;
+
+public interface Objc extends Library {
+ Objc INSTANCE = Native.load("objc", Objc.class);
+ NativeLong objc_lookUpClass(String name);
+ Pointer sel_getUid(String str);
+ NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Object... args);
+}
\ No newline at end of file
diff --git a/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java
index d27b6a3c8..877634657 100644
--- a/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java
+++ b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java
@@ -72,7 +72,7 @@ protected void showJPopupMenu(MouseEvent mouseEvent) {
if (menu != null) {
Point location = mouseEvent.getLocationOnScreen();
// Handle HiDPI factor discrepancy between mouse position and window position
- if(SystemUtilities.isWindows() && Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("9.0.0"))) {
+ if(SystemUtilities.isWindows() && Constants.JAVA_VERSION.getMajorVersion() >= 9) {
location.setLocation(location.getX() / WindowsUtilities.getScaleFactor(), location.getY() / WindowsUtilities.getScaleFactor());
}
showJPopupMenu((int)location.getX(), (int)location.getY());
@@ -162,25 +162,29 @@ public void actionPerformed(ActionEvent e) {
@Override
public Dimension getSize() {
Dimension iconSize = new Dimension(super.getSize());
- // macOS edge-cases
- if (SystemUtilities.isMac()) {
- // Handle retina display
- int scale = MacUtilities.getScaleFactor();
-
- // Handle undocumented icon border (e.g. 20px has 16px icon)
- // See also IconCache.fixTrayIcons()
- iconSize.width -= iconSize.width / 5;
- iconSize.height -= iconSize.height / 5;
-
- return new Dimension(iconSize.width * scale, iconSize.height * scale);
- } else if(SystemUtilities.isWindows() && Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("9.0.0"))) {
- // JDK9+ required for HiDPI tray icons on Windows
- int scale = WindowsUtilities.getScaleFactor();
-
- // Handle undocumented HiDPI icon support
- // Requires TrayIcon.setImageAutoSize(true);
- iconSize.width *= scale;
- iconSize.height *= scale;
+ switch(SystemUtilities.getOsType()) {
+ // macOS icons are slightly smaller than the size reported
+ case MAC:
+ // Handle retina display
+ int macScale = MacUtilities.getScaleFactor();
+
+ // Handle undocumented icon border (e.g. 20px has 16px icon)
+ // See also IconCache.fixTrayIcons()
+ iconSize.width -= iconSize.width / 5;
+ iconSize.height -= iconSize.height / 5;
+
+ return new Dimension(iconSize.width * macScale, iconSize.height * macScale);
+ case WINDOWS:
+ if(Constants.JAVA_VERSION.getMajorVersion() >= 9) {
+ // JDK9+ required for HiDPI tray icons on Windows
+ int winScale = WindowsUtilities.getScaleFactor();
+
+ // Handle undocumented HiDPI icon support
+ // Requires TrayIcon.setImageAutoSize(true);
+ iconSize.width *= winScale;
+ iconSize.height *= winScale;
+ }
+ break;
}
return iconSize;
}
diff --git a/src/qz/App.java b/src/qz/App.java
new file mode 100644
index 000000000..fe7e4c1ff
--- /dev/null
+++ b/src/qz/App.java
@@ -0,0 +1,97 @@
+package qz;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.rolling.FixedWindowRollingPolicy;
+import org.apache.log4j.rolling.RollingFileAppender;
+import org.apache.log4j.rolling.SizeBasedTriggeringPolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.Installer;
+import qz.installer.certificate.CertificateManager;
+import qz.installer.certificate.ExpiryTask;
+import qz.installer.certificate.KeyPairWrapper;
+import qz.installer.certificate.NativeCertificateInstaller;
+import qz.utils.*;
+import qz.ws.PrintSocketServer;
+import qz.ws.SingleInstanceChecker;
+
+import java.io.File;
+import java.util.Properties;
+
+public class App {
+ private static final Logger log = LoggerFactory.getLogger(App.class);
+ private static Properties trayProperties = null;
+
+ public static void main(String ... args) {
+ ArgParser parser = new ArgParser(args);
+ LibUtilities.getInstance().bind();
+ if(parser.intercept()) {
+ System.exit(parser.getExitCode());
+ }
+ SingleInstanceChecker.stealWebsocket = parser.hasFlag(ArgValue.STEAL);
+ setupFileLogging();
+ log.info(Constants.ABOUT_TITLE + " version: {}", Constants.VERSION);
+ log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY);
+ log.info("Java version: {}", Constants.JAVA_VERSION.toString());
+ log.info("Java vendor: {}", Constants.JAVA_VENDOR);
+
+ CertificateManager certManager = null;
+ try {
+ // Gets and sets the SSL info, properties file
+ certManager = Installer.getInstance().certGen(false);
+ trayProperties = certManager.getProperties();
+ // Reoccurring (e.g. hourly) cert expiration check
+ new ExpiryTask(certManager).schedule();
+ } catch(Exception e) {
+ log.error("Something went critically wrong loading HTTPS", e);
+ }
+ Installer.getInstance().addUserSettings();
+
+ // Load overridable preferences set in qz-tray.properties file
+ NetworkUtilities.setPreferences(certManager.getProperties());
+ SingleInstanceChecker.setPreferences(certManager.getProperties());
+
+ // Linux needs the cert installed in user-space on every launch for Chrome SSL to work
+ if(!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
+ NativeCertificateInstaller.getInstance().install(certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert());
+ }
+
+ try {
+ log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
+ // Start the WebSocket
+ PrintSocketServer.runServer(certManager, parser.isHeadless());
+ }
+ catch(Exception e) {
+ log.error("Could not start tray manager", e);
+ }
+
+ log.warn("The web socket server is no longer running");
+ }
+
+ public static Properties getTrayProperties() {
+ return trayProperties;
+ }
+
+ private static void setupFileLogging() {
+ FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
+ rollingPolicy.setFileNamePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log.%i");
+ rollingPolicy.setMaxIndex(Constants.LOG_ROTATIONS);
+
+ SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy(Constants.LOG_SIZE);
+
+ RollingFileAppender fileAppender = new RollingFileAppender();
+ fileAppender.setLayout(new PatternLayout("%d{ISO8601} [%p] %m%n"));
+ fileAppender.setThreshold(Level.DEBUG);
+ fileAppender.setFile(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log");
+ fileAppender.setRollingPolicy(rollingPolicy);
+ fileAppender.setTriggeringPolicy(triggeringPolicy);
+ fileAppender.setEncoding("UTF-8");
+
+ fileAppender.setImmediateFlush(true);
+ fileAppender.activateOptions();
+
+ org.apache.log4j.Logger.getRootLogger().addAppender(fileAppender);
+ }
+}
diff --git a/src/qz/auth/Certificate.java b/src/qz/auth/Certificate.java
index 9ce511d04..5f2962176 100644
--- a/src/qz/auth/Certificate.java
+++ b/src/qz/auth/Certificate.java
@@ -7,21 +7,19 @@
import org.apache.commons.ssl.X509CertificateChainBuilder;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.style.BCStyle;
-import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import qz.App;
import qz.common.Constants;
import qz.utils.ByteUtilities;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
-import qz.ws.PrintSocketServer;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.security.*;
import java.security.cert.*;
import java.time.DateTimeException;
@@ -138,18 +136,16 @@ public enum Algorithm {
}
}
-
public static void scanAdditionalCAs() {
ArrayList
> certPaths = new ArrayList<>();
// First, look for "-DtrustedRootCert" command line property
certPaths.addAll(FileUtilities.parseDelimitedPaths(System.getProperty(OVERRIDE_CA_FLAG)));
// Second, look for "override.crt" within App directory
- String override = FileUtilities.getParentDirectory(SystemUtilities.getJarPath()) + File.separator + Constants.OVERRIDE_CERT;
- certPaths.add(new AbstractMap.SimpleEntry<>(Paths.get(override), QUIETLY_FAIL));
+ certPaths.add(new AbstractMap.SimpleEntry<>(SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT), QUIETLY_FAIL));
// Third, look for "authcert.override" property in qz-tray.properties
- certPaths.addAll(FileUtilities.parseDelimitedPaths(PrintSocketServer.getTrayProperties(), OVERRIDE_CA_PROPERTY));
+ certPaths.addAll(FileUtilities.parseDelimitedPaths(App.getTrayProperties(), OVERRIDE_CA_PROPERTY));
for(Map.Entry certPath : certPaths) {
if(certPath.getKey() != null) {
diff --git a/src/qz/build/Fetcher.java b/src/qz/build/Fetcher.java
new file mode 100644
index 000000000..237cef941
--- /dev/null
+++ b/src/qz/build/Fetcher.java
@@ -0,0 +1,129 @@
+package qz.build;
+
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * Fetches a zip or tarball from URL and decompresses it
+ */
+public class Fetcher {
+ public enum Format {
+ ZIP(".zip"),
+ TARBALL(".tar.gz"),
+ UNKNOWN(null);
+
+ String suffix;
+ Format(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public static Format parse(String url) {
+ for(Format format : Format.values()) {
+ if (url.endsWith(format.getSuffix())) {
+ return format;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(Fetcher.class);
+
+ public static void main(String ... args) throws IOException {
+ new Fetcher("jlink/qz-tray-src_x.x.x", "https://github.com/qzind/tray/archive/master.tar.gz").fetch().uncompress();
+ }
+
+ String resourceName;
+ String url;
+ Format format;
+ Path rootDir;
+ File tempArchive;
+ File tempExtracted;
+ File extracted;
+
+ public Fetcher(String resourceName, String url) {
+ this.url = url;
+ this.resourceName = resourceName;
+ this.format = Format.parse(url);
+ // Try to calculate out/
+ this.rootDir = SystemUtilities.getJarParentPath().getParent();
+ }
+
+ @SuppressWarnings("unused")
+ public Fetcher(String resourceName, String url, Format format, String rootDir) {
+ this.resourceName = resourceName;
+ this.url = url;
+ this.format = format;
+ this.rootDir = Paths.get(rootDir);
+ }
+
+ public Fetcher fetch() throws IOException {
+ extracted = new File(rootDir.toString(), resourceName);
+ if(extracted.isDirectory() && extracted.exists()) {
+ log.info("Resource '{}' from [{}] has already been downloaded and extracted. Using: [{}]", resourceName, url, extracted);
+ } else {
+ tempExtracted = new File(rootDir.toString(), resourceName + "~tmp");
+ if(tempExtracted.exists()) {
+ FileUtils.deleteDirectory(tempExtracted);
+ }
+ // temp directory to thwart partial extraction
+ tempExtracted.mkdirs();
+ tempArchive = File.createTempFile(resourceName, ".zip");
+ log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempArchive);
+ FileUtils.copyURLToFile(new URL(url), tempArchive);
+ }
+ return this;
+ }
+
+ public String uncompress() throws IOException {
+ if(tempArchive != null) {
+ log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempArchive, tempExtracted);
+ if(format == Format.ZIP) {
+ unzip(tempArchive.getAbsolutePath(), tempExtracted);
+ } else {
+ untar(tempArchive.getAbsolutePath(), tempExtracted);
+ }
+ log.info("Moving [{}] to [{}]", tempExtracted, extracted);
+ tempExtracted.renameTo(extracted);
+ }
+ return extracted.toString();
+ }
+
+ public static void untar(String sourceFile, File targetDir) throws IOException {
+ // TODO: Switch to TarArchiveInputStream from Apache Commons Compress
+ if (!ShellUtilities.execute("tar", "-xzf", sourceFile, "-C", targetDir.getPath())) {
+ throw new IOException("Something went wrong extracting " + sourceFile +", check logs for details");
+ }
+ }
+
+ public static void unzip(String sourceFile, File targetDir) throws IOException {
+ try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(sourceFile))) {
+ for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
+ Path resolvedPath = targetDir.toPath().resolve(ze.getName());
+ if (ze.isDirectory()) {
+ Files.createDirectories(resolvedPath);
+ } else {
+ Files.createDirectories(resolvedPath.getParent());
+ Files.copy(zipIn, resolvedPath);
+ }
+ }
+ }
+ }
+}
diff --git a/src/qz/build/JLink.java b/src/qz/build/JLink.java
new file mode 100644
index 000000000..446fcd99d
--- /dev/null
+++ b/src/qz/build/JLink.java
@@ -0,0 +1,296 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2020 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.build;
+
+import com.github.zafarkhaja.semver.Version;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.*;
+import qz.common.Constants;
+import qz.utils.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+public class JLink {
+ private static final Logger log = LoggerFactory.getLogger(JLink.class);
+ private static final String JAVA_AMD64_VENDOR = "AdoptOpenJDK";
+ private static final String JAVA_ARM64_VENDOR = "BellSoft";
+ private static final String JAVA_VERSION = "11.0.12+7";
+ private static final String JAVA_MAJOR = JAVA_VERSION.split("\\.")[0];
+ private static final String JAVA_MINOR = JAVA_VERSION.split("\\.")[1];
+ private static final String JAVA_PATCH = JAVA_VERSION.split("\\.|\\+|-")[2];
+ private static final String JAVA_VERSION_FILE = JAVA_VERSION.replaceAll("\\+", "_");
+ private static final String JAVA_DEFAULT_GC_ENGINE = "hotspot";
+ private static final String JAVA_DEFAULT_ARCH = VendorArch.ADOPT_AMD64.use;
+
+ private Path jarPath;
+ private Path jdepsPath;
+ private Path jlinkPath;
+ private Path jmodsPath;
+ private Path outPath;
+ private Version jdepsVersion;
+ private String javaVendor;
+ private String targetPlatform;
+ private LinkedHashSet depList;
+
+ public JLink(String targetPlatform, String arch, String gcEngine) throws IOException {
+ this.javaVendor = getJavaVendor(arch);
+ this.targetPlatform = targetPlatform;
+
+ // jdeps and jlink require matching major JDK versions. Download if needed.
+ if(Constants.JAVA_VERSION.getMajorVersion() != Integer.parseInt(JAVA_MAJOR)) {
+ log.warn("Java versions are incompatible, locating a suitable runtime for Java " + JAVA_MAJOR + "...");
+ String hostArch = System.getProperty("os.arch");
+ String hostJdk = downloadJdk(getJavaVendor(hostArch), null, hostArch, gcEngine);
+ calculateToolPaths(Paths.get(hostJdk));
+ } else {
+ calculateToolPaths(null);
+ }
+
+ String extractedJdk = downloadJdk(javaVendor, targetPlatform, arch, gcEngine);
+ jmodsPath = Paths.get(extractedJdk, "jmods");
+ log.info("Selecting jmods folder: {}", jmodsPath);
+
+ calculateJarPath()
+ .calculateOutPath()
+ .calculateDepList()
+ .deployJre();
+ }
+
+ public static void main(String ... args) throws IOException {
+ JLink jlink = new JLink(null, null, null).calculateJarPath();
+ System.out.println(jlink.jarPath);
+ if(true) {
+ System.exit(0);
+ }
+
+
+ new JLink(args.length > 0 ? args[0] : null,
+ args.length > 1 ? args[1] : null,
+ args.length > 2 ? args[2] : null);
+ }
+
+ private String getJavaVendor(String arch) {
+ String vendor;
+ switch(SystemUtilities.getJreArch(arch)) {
+ case AARCH64:
+ vendor = JAVA_ARM64_VENDOR;
+ break;
+ case X86_64:
+ default:
+ vendor = JAVA_AMD64_VENDOR;
+ }
+ return vendor;
+ }
+
+ /**
+ * Download the JDK and return the path it was extracted to
+ */
+ private static String downloadJdk(String javaVendor, String platform, String arch, String gcEngine) throws IOException {
+ if(platform == null) {
+ // Must match ArgValue.JLINK --platform values
+ switch(SystemUtilities.getOsType()) {
+ case MAC:
+ platform = "mac";
+ break;
+ case WINDOWS:
+ platform = "windows";
+ break;
+ default:
+ platform = "linux";
+ }
+
+ log.info("No platform specified, assuming '{}'", platform);
+ }
+
+ arch = VendorArch.match(javaVendor, arch, JAVA_DEFAULT_ARCH);
+ platform = VendorOs.match(javaVendor, platform);
+
+
+ if(gcEngine == null) {
+ gcEngine = JAVA_DEFAULT_GC_ENGINE;
+ log.info("No garbage collector specified, assuming '{}'", gcEngine);
+ }
+
+ String fileExt;
+ switch(VendorUrlPattern.getVendor(javaVendor)) {
+ case BELL:
+ fileExt = platform.equals("linux") ? "tar.gz" : "zip";
+ break;
+ case ADOPT:
+ default:
+ fileExt = platform.equals("windows") ? "zip" : "tar.gz";
+ }
+
+ String url = VendorUrlPattern.format(javaVendor, arch, platform, gcEngine, JAVA_MAJOR, JAVA_VERSION, JAVA_VERSION_FILE, fileExt);
+
+ // Saves to out e.g. "out/jlink/jdk-AdoptOpenjdk-amd64-platform-11_0_7"
+ String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.toLowerCase(Locale.ENGLISH), arch, platform, JAVA_VERSION_FILE), url)
+ .fetch()
+ .uncompress();
+
+ // Get first subfolder, e.g. jdk-11.0.7+10
+ for(File subfolder : new File(extractedJdk).listFiles(pathname -> pathname.isDirectory())) {
+ extractedJdk = subfolder.getPath();
+ if(platform.equals("mac") && Paths.get(extractedJdk, "/Contents/Home").toFile().isDirectory()) {
+ extractedJdk += "/Contents/Home";
+ }
+ log.info("Selecting JDK home: {}", extractedJdk);
+ break;
+ }
+
+ return extractedJdk;
+ }
+
+ private JLink calculateJarPath() {
+ if(SystemUtilities.isJar()) {
+ jarPath = SystemUtilities.getJarPath();
+ } else {
+ // Detect out/dist/qz-tray.jar for IDE usage
+ jarPath = SystemUtilities.getJarParentPath()
+ .resolve("../../")
+ .resolve(Constants.PROPS_FILE + ".jar");
+ }
+ log.info("Assuming jar path: {}", jarPath);
+ return this;
+ }
+
+ private JLink calculateOutPath() {
+ if(targetPlatform.equals("mac")) {
+ outPath = jarPath.resolve("../Java.runtime/Contents/Home").normalize();
+ } else {
+ outPath = jarPath.resolve("../runtime").normalize();
+ }
+ log.info("Assuming output path: {}", outPath);
+ return this;
+ }
+
+ private JLink calculateToolPaths(Path javaHome) throws IOException {
+ if(javaHome == null) {
+ javaHome = Paths.get(System.getProperty("java.home"));
+ }
+ log.info("Using JAVA_HOME: {}", javaHome);
+ jdepsPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jdeps.exe" : "jdeps").normalize();
+ jlinkPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jlink.exe" : "jlink").normalize();
+ log.info("Assuming jdeps path: {}", jdepsPath);
+ log.info("Assuming jlink path: {}", jlinkPath);
+ jdepsPath.toFile().setExecutable(true, false);
+ jlinkPath.toFile().setExecutable(true, false);
+ jdepsVersion = SystemUtilities.getJavaVersion(jdepsPath);
+ return this;
+ }
+
+ private JLink calculateDepList() throws IOException {
+ log.info("Calling jdeps to determine runtime dependencies");
+ depList = new LinkedHashSet<>();
+
+ // JDK11.0.11+requires suppressing of missing deps
+ String raw = jdepsVersion.compareTo(Version.valueOf("11.0.10")) > 0 ?
+ ShellUtilities.executeRaw(jdepsPath.toString(), "--list-deps", "--ignore-missing-deps", jarPath.toString()) :
+ ShellUtilities.executeRaw(jdepsPath.toString(), "--list-deps", jarPath.toString());
+ if (raw == null || raw.trim().isEmpty() || raw.trim().startsWith("Warning") ) {
+ throw new IOException("An unexpected error occurred calling jdeps. Please check the logs for details.\n" + raw);
+ }
+ for(String item : raw.split("\\r?\\n")) {
+ item = item.trim();
+ if(!item.isEmpty()) {
+ if(item.startsWith("JDK") || item.startsWith("jdk8internals")) {
+ // Remove e.g. "JDK removed internal API/sun.reflect"
+ log.trace("Removing dependency: '{}'", item);
+ continue;
+ }
+ if(item.contains("/")) {
+ // Isolate base name e.g. "java.base/com.sun.net.ssl"
+ item = item.split("/")[0];
+ }
+ depList.add(item);
+ }
+ }
+ // "jar:" URLs create transient zipfs dependency, see https://stackoverflow.com/a/57846672/3196753
+ depList.add("jdk.zipfs");
+ return this;
+ }
+
+ private JLink deployJre() throws IOException {
+ if(targetPlatform.equals("mac")) {
+ // Deploy Contents/MacOS/libjli.dylib
+ Path macOS = Files.createDirectories(outPath.resolve("../MacOS").normalize());
+ Path jliLib = macOS.resolve("libjli.dylib");
+ log.info("Deploying {}", macOS);
+ try {
+ // Not all jdks use a bundle format, but try this first
+ Files.copy(jmodsPath.resolve("../../MacOS/libjli.dylib").normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
+ } catch(IOException ignore) {
+ // Fallback to flat format
+ Files.copy(jmodsPath.resolve("../lib/jli/libjli.dylib").normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ // Deploy Contents/Info.plist
+ HashMap fieldMap = new HashMap<>();
+ fieldMap.put("%BUNDLE_ID%", MacUtilities.getBundleId() + ".jre"); // e.g. io.qz.qz-tray.jre
+ fieldMap.put("%BUNDLE_VERSION%", String.format("%s.%s.%s", JAVA_MAJOR, JAVA_MINOR, JAVA_PATCH));
+ fieldMap.put("%BUNDLE_VERSION_FULL%", JAVA_VERSION);
+ fieldMap.put("%BUNDLE_VENDOR%", javaVendor);
+ log.info("Deploying {}/Info.plist", macOS.getParent());
+ FileUtilities.configureAssetFile("assets/mac-runtime.plist.in", macOS.getParent().resolve("Info.plist"), fieldMap, JLink.class);
+ }
+
+ FileUtils.deleteQuietly(outPath.toFile());
+
+ if(ShellUtilities.execute(jlinkPath.toString(),
+ "--strip-debug",
+ "--compress=2",
+ "--no-header-files",
+ "--no-man-pages",
+ "--exclude-files=glob:**/legal/**",
+ "--module-path", jmodsPath.toString(),
+ "--add-modules", String.join(",", depList),
+ "--output", outPath.toString())) {
+ log.info("Successfully deployed a jre to {}", outPath);
+
+ // Remove all but java/javaw
+ String[] keepFiles;
+ String keepExt;
+ if(targetPlatform.equals("windows")) {
+ keepFiles = new String[]{ "java.exe", "javaw.exe" };
+ // Windows stores ".dll" files in bin
+ keepExt = ".dll";
+ } else {
+ keepFiles = new String[]{ "java" };
+ keepExt = null;
+ }
+
+ Files.list(outPath.resolve("bin")).forEach(binFile -> {
+ if(Files.isDirectory(binFile) || (keepExt != null && binFile.toString().endsWith(keepExt))) {
+ log.info("Keeping {}", binFile);
+ return; // iterate forEach
+ }
+ for(String name : keepFiles) {
+ if (binFile.endsWith(name)) {
+ log.info("Keeping {}", binFile);
+ return; // iterate forEach
+ }
+ }
+ log.info("Deleting {}", binFile);
+ binFile.toFile().delete();
+ });
+
+ return this;
+
+ }
+ throw new IOException("An error occurred deploying the jre. Please check the logs for details.");
+ }
+
+}
diff --git a/src/qz/build/VendorArch.java b/src/qz/build/VendorArch.java
new file mode 100644
index 000000000..bf7f43cf5
--- /dev/null
+++ b/src/qz/build/VendorArch.java
@@ -0,0 +1,50 @@
+package qz.build;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+/**
+ * Each JDK provider uses their own architecture aliases
+ * e.g. Adopt uses "x64" whereas BellSoft uses "amd64".
+ */
+public enum VendorArch {
+ // Values must contain underscore
+
+ // AMD64
+ ADOPT_AMD64("x64", "amd64", "x86_64", "x64"),
+ BELL_AMD64("amd64", "amd64", "x86_64", "x64"),
+
+ // ARM64
+ ADOPT_AARCH64("aarch64", "aarch64", "arm64"),
+ BELL_AARCH64("aarch64", "aarch64", "arm64");
+
+ private static final Logger log = LoggerFactory.getLogger(VendorArch.class);
+
+ String use;
+ String[] matches;
+ VendorArch(String use, String ... matches) {
+ this.use = use;
+ this.matches = matches;
+ }
+
+ public static String match(String vendor, String arch, String fallback) {
+ if(arch != null && vendor != null) {
+ for(VendorArch alias : values()) {
+ String vendorPrefix = alias.name().split("_")[0].toLowerCase(Locale.ROOT);
+ if (vendor.toLowerCase(Locale.ROOT).startsWith(vendorPrefix)) {
+ for(String match : alias.matches) {
+ if (arch.equalsIgnoreCase(match)) {
+ log.info("Arch provided: {} matches: {}", arch, match);
+ return alias.use;
+ }
+ }
+ }
+ }
+ }
+ log.warn("Arch provided couldn't be matched: {} falling back to: {}", arch, fallback);
+ return fallback;
+ }
+
+}
\ No newline at end of file
diff --git a/src/qz/build/VendorOs.java b/src/qz/build/VendorOs.java
new file mode 100644
index 000000000..ffcabbca1
--- /dev/null
+++ b/src/qz/build/VendorOs.java
@@ -0,0 +1,51 @@
+package qz.build;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+/**
+ * Each JDK provider uses their own os aliases
+ * e.g. Adopt uses "mac" whereas BellSoft uses "macos".
+ */
+public enum VendorOs {
+ // Values must contain underscore
+
+ // MacOS
+ ADOPT_MACOS("mac", "mac"),
+ BELL_MACOS("macos", "mac");
+
+ // Windows
+ // (skip, all vendors use "windows")
+
+ // Linux
+ // (skip, all vendors use "linux")
+
+ private static final Logger log = LoggerFactory.getLogger(VendorArch.class);
+
+ String use;
+ String[] matches;
+ VendorOs(String use, String ... matches) {
+ this.use = use;
+ this.matches = matches;
+ }
+
+ public static String match(String vendor, String os) {
+ if(os != null && vendor != null) {
+ for(VendorOs alias : values()) {
+ String vendorPrefix = alias.name().split("_")[0].toLowerCase(Locale.ROOT);
+ if (vendor.toLowerCase(Locale.ROOT).startsWith(vendorPrefix)) {
+ for(String match : alias.matches) {
+ if (os.equalsIgnoreCase(match)) {
+ log.info("OS provided: {} matches: {}", os, match);
+ return alias.use;
+ }
+ }
+ }
+ }
+ }
+ return os;
+ }
+
+}
\ No newline at end of file
diff --git a/src/qz/build/VendorUrlPattern.java b/src/qz/build/VendorUrlPattern.java
new file mode 100644
index 000000000..93dabfd80
--- /dev/null
+++ b/src/qz/build/VendorUrlPattern.java
@@ -0,0 +1,46 @@
+package qz.build;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+/**
+ * Each JDK provider uses their own url format
+ */
+public enum VendorUrlPattern {
+ ADOPT("https://github.com/adoptium/temurin%s-binaries/releases/download/jdk-%s/OpenJDK%sU-jdk_%s_%s_%s_%s.%s"),
+ BELL("https://download.bell-sw.com/java/%s/bellsoft-jdk%s-%s-%s.%s");
+
+ private static final VendorUrlPattern DEFAULT_VENDOR = ADOPT;
+ private static final Logger log = LoggerFactory.getLogger(VendorUrlPattern.class);
+
+ String pattern;
+ VendorUrlPattern(String pattern) {
+ this.pattern = pattern;
+ }
+
+ public static VendorUrlPattern getVendor(String vendor) {
+ if(vendor != null) {
+ for(VendorUrlPattern pattern : values()) {
+ if (vendor.toUpperCase(Locale.ROOT).startsWith(pattern.name())) {
+ return pattern;
+ }
+ }
+ }
+ log.warn("Vendor provided couldn't be matched: {} will fallback to default: {}", vendor, DEFAULT_VENDOR);
+ return null;
+ }
+
+ public static String format(String vendor, String arch, String platform, String gcEngine, String javaMajor, String javaVersion, String javaVersionFormatted, String fileExt) {
+ VendorUrlPattern pattern = VendorUrlPattern.getVendor(vendor);
+ switch(pattern) {
+ case BELL:
+ return String.format(pattern.pattern, javaVersion, javaVersion, platform, arch, fileExt);
+ case ADOPT:
+ default:
+ return String.format(pattern.pattern, javaMajor, javaVersion, javaMajor, arch, platform, gcEngine, javaVersionFormatted, fileExt);
+ }
+ }
+
+}
diff --git a/src/qz/build/assets/mac-runtime.plist.in b/src/qz/build/assets/mac-runtime.plist.in
new file mode 100644
index 000000000..c7e1503e4
--- /dev/null
+++ b/src/qz/build/assets/mac-runtime.plist.in
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ libjli.dylib
+ CFBundleGetInfoString
+ %BUNDLE_VENDOR% %BUNDLE_VERSION_FULL%
+ CFBundleIdentifier
+ %BUNDLE_ID%
+ CFBundleInfoDictionaryVersion
+ 7.0
+ CFBundleName
+ Java Runtime Image
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ %BUNDLE_VERSION%
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ %BUNDLE_VERSION%
+ JavaVM
+
+ JVMCapabilities
+
+ CommandLine
+ JNI
+ BundledApp
+
+ JVMMinimumFrameworkVersion
+ 17.0.0
+ JVMMinimumSystemVersion
+ 10.6.0
+ JVMPlatformVersion
+ %BUNDLE_VERSION_FULL%
+ JVMVendor
+ %BUNDLE_VENDOR%
+ JVMVersion
+ %BUNDLE_VERSION%
+
+
+
diff --git a/src/qz/common/AboutInfo.java b/src/qz/common/AboutInfo.java
index a5dd48b67..d4218dd4e 100644
--- a/src/qz/common/AboutInfo.java
+++ b/src/qz/common/AboutInfo.java
@@ -82,8 +82,10 @@ private static JSONObject environment() throws JSONException {
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
environment
- .put("os", SystemUtilities.getOS())
- .put("java", String.format("%s (%s)", Constants.JAVA_VERSION, System.getProperty("os.arch")))
+ .put("os", SystemUtilities.getOsType().toString().toLowerCase())
+ .put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getJreArch().toString().toLowerCase()))
+ .put("java (location)", System.getProperty("java.home"))
+ .put("java (vendor)", Constants.JAVA_VENDOR)
.put("uptime", DurationFormatUtils.formatDurationWords(uptime, true, false))
.put("uptimeMillis", uptime);
diff --git a/src/qz/common/Constants.java b/src/qz/common/Constants.java
index 25bf78d8c..3ae4508d3 100644
--- a/src/qz/common/Constants.java
+++ b/src/qz/common/Constants.java
@@ -14,7 +14,7 @@ public class Constants {
public static final String HEXES = "0123456789ABCDEF";
public static final char[] HEXES_ARRAY = HEXES.toCharArray();
public static final int BYTE_BUFFER_SIZE = 8192;
- public static final Version VERSION = Version.valueOf("2.1.4");
+ public static final Version VERSION = Version.valueOf("2.2.0-SNAPSHOT");
public static final Version JAVA_VERSION = SystemUtilities.getJavaVersion();
public static final String JAVA_VENDOR = System.getProperty("java.vendor");
diff --git a/src/qz/common/PropertyHelper.java b/src/qz/common/PropertyHelper.java
index fce7f8f93..17ccdda33 100644
--- a/src/qz/common/PropertyHelper.java
+++ b/src/qz/common/PropertyHelper.java
@@ -7,7 +7,6 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
-import java.util.logging.Level;
/**
* Created by Tres on 12/16/2015.
*/
diff --git a/src/qz/common/SecurityInfo.java b/src/qz/common/SecurityInfo.java
index c88b8d34b..2aa30482d 100644
--- a/src/qz/common/SecurityInfo.java
+++ b/src/qz/common/SecurityInfo.java
@@ -5,6 +5,7 @@
import org.eclipse.jetty.util.Jetty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.usb4java.LibUsb;
import purejavahidapi.PureJavaHidApi;
import qz.utils.SystemUtilities;
@@ -12,6 +13,7 @@
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
+import java.net.URLDecoder;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.GeneralSecurityException;
@@ -38,8 +40,8 @@ public V put(K key, CheckedValue value) {
return put(key, (V)value.check());
} catch(Throwable t) {
log.warn("A checked exception was suppressed adding key \"{}\"", key, t);
+ return put(key, (V)"missing");
}
- return null;
}
}
@@ -68,35 +70,38 @@ public static SortedMap getLibVersions() {
// Use API-provided mechanism if available
libVersions.put("jna (native)", () -> Native.VERSION_NATIVE);
+ libVersions.put("jna (location)", () -> {
+ @SuppressWarnings("unused")
+ int ignore = Native.BOOL_SIZE;
+ return System.getProperty("jnidispatch.path");
+ });
libVersions.put("jna", Native.VERSION);
libVersions.put("jssc", () -> jssc.SerialNativeInterface.getLibraryVersion());
+ libVersions.put("jssc (native)", () -> jssc.SerialNativeInterface.getNativeLibraryVersion());
libVersions.put("jetty", Jetty.VERSION);
libVersions.put("pdfbox", org.apache.pdfbox.util.Version.getVersion());
libVersions.put("purejavahidapi", () -> PureJavaHidApi.getVersion());
libVersions.put("usb-api", javax.usb.Version.getApiVersion());
libVersions.put("not-yet-commons-ssl", org.apache.commons.ssl.Version.VERSION);
libVersions.put("mslinks", mslinks.ShellLink.VERSION);
- libVersions.put("simplersa", (String)null);
libVersions.put("bouncycastle", "" + new BouncyCastleProvider().getVersion());
+ libVersions.put("usb4java (native)", () -> LibUsb.getVersion().toString());
libVersions.put("jre", Constants.JAVA_VERSION.toString());
libVersions.put("jre (vendor)", Constants.JAVA_VENDOR);
//JFX info, if it exists
try {
+ // "DO NOT LINK JAVAFX EVER" - JavaFX may not exist, use reflection to avoid compilation errors
Class> VersionInfo = Class.forName("com.sun.javafx.runtime.VersionInfo");
- String fxPath = VersionInfo.getProtectionDomain().getCodeSource().getLocation().toString();
+ Path fxPath = Paths.get(VersionInfo.getProtectionDomain().getCodeSource().getLocation().toURI());
Method method = VersionInfo.getMethod("getVersion");
Object version = method.invoke(null);
libVersions.put("javafx", (String)version);
- if (fxPath.contains(SystemUtilities.detectJarPath()) || fxPath.contains("/tray/")) {
- libVersions.put("javafx (location)", "Bundled/" + Constants.ABOUT_TITLE);
- } else {
- libVersions.put("javafx (location)", "System/" + Constants.JAVA_VENDOR);
- }
+ libVersions.put("javafx (location)", fxPath.toString());
} catch(Throwable e) {
- libVersions.put("javafx", "Failed");
- libVersions.put("javafx (location)", "Failed");
+ libVersions.put("javafx", "missing");
+ libVersions.put("javafx (location)", "missing");
}
// Fallback to maven manifest information
@@ -106,7 +111,7 @@ public static SortedMap getLibVersions() {
"slf4j-log4j12", "usb4java-javax", "java-semver", "commons-pool2",
"websocket-server", "jettison", "commons-codec", "log4j", "slf4j-api",
"websocket-servlet", "jetty-http", "commons-lang3", "javax-websocket-server-impl",
- "javax.servlet-api", "usb4java", "websocket-api", "jetty-util", "websocket-client",
+ "javax.servlet-api", "hid4java", "usb4java", "websocket-api", "jetty-util", "websocket-client",
"javax.websocket-api", "commons-io", "jetty-security"};
for(String lib : mavenLibs) {
@@ -138,7 +143,7 @@ private static HashMap getMavenVersions() {
final HashMap mavenVersions = new HashMap<>();
String jar = "jar:" + SecurityInfo.class.getProtectionDomain().getCodeSource().getLocation().toString();
try(FileSystem fs = FileSystems.newFileSystem(new URI(jar), new HashMap())) {
- Files.walkFileTree(fs.getPath("/META-INF/maven"), new HashSet(), 3, new SimpleFileVisitor() {
+ Files.walkFileTree(fs.getPath("/META-INF/maven"), new HashSet<>(), 3, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".properties")) {
diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java
index a612eeffe..bb65a05b5 100644
--- a/src/qz/common/TrayManager.java
+++ b/src/qz/common/TrayManager.java
@@ -11,9 +11,7 @@
package qz.common;
import com.github.zafarkhaja.semver.Version;
-import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.auth.Certificate;
@@ -37,8 +35,6 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
/**
* Manages the icons and actions associated with the TrayIcon
@@ -110,14 +106,17 @@ public TrayManager(boolean isHeadless) {
iconCache = new IconCache();
if (!headless && SystemTray.isSupported()) { // UI mode with tray
- if (SystemUtilities.isWindows()) {
- tray = TrayType.JX.init();
- // Undocumented HiDPI behavior
- tray.setImageAutoSize(true);
- } else if (SystemUtilities.isMac()) {
- tray = TrayType.CLASSIC.init();
- } else {
- tray = TrayType.MODERN.init();
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ tray = TrayType.JX.init();
+ // Undocumented HiDPI behavior
+ tray.setImageAutoSize(true);
+ break;
+ case MAC:
+ tray = TrayType.CLASSIC.init();
+ break;
+ default:
+ tray = TrayType.MODERN.init();
}
// OS-specific tray icon handling
@@ -247,7 +246,7 @@ private void addMenuItems() {
JMenuItem diagnosticMenu = new JMenu("Diagnostic");
JMenuItem browseApp = new JMenuItem("Browse App folder...", iconCache.getIcon(IconCache.Icon.FOLDER_ICON));
- browseApp.setToolTipText(FileUtilities.getParentDirectory(SystemUtilities.getJarPath()));
+ browseApp.setToolTipText(SystemUtilities.getJarParentPath().toString());
browseApp.setMnemonic(KeyEvent.VK_O);
browseApp.addActionListener(e -> ShellUtilities.browseAppDirectory());
diagnosticMenu.add(browseApp);
@@ -521,59 +520,21 @@ private void blackList(Certificate cert) {
}
}
- /**
- * Sets the WebSocket Server instance for displaying port information and restarting the server
- *
- * @param server The Server instance contain to bind the reload action to
- * @param running Object used to notify PrintSocket to reiterate its main while loop
- * @param securePortIndex Object used to notify PrintSocket to reset its port array counter
- * @param insecurePortIndex Object used to notify PrintSocket to reset its port array counter
- */
- public void setServer(final Server server, final AtomicBoolean running, final AtomicInteger securePortIndex, final AtomicInteger insecurePortIndex) {
+ public void setServer(Server server, int insecurePortIndex) {
if (server != null && server.getConnectors().length > 0) {
- singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortIndex.get());
+ singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortIndex);
- displayInfoMessage("Server started on port(s) " + TrayManager.getPorts(server));
+ displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
if (!headless) {
aboutDialog.setServer(server);
setDefaultIcon();
}
-
- setReloadThread(new Thread(() -> {
- try {
- setDangerIcon();
- running.set(false);
- securePortIndex.set(0);
- insecurePortIndex.set(0);
-
- server.stop();
- }
- catch(Exception e) {
- displayErrorMessage("Error stopping print socket: " + e.getLocalizedMessage());
- }
- }));
} else {
displayErrorMessage("Invalid server");
}
}
- /**
- * Returns a String representation of the ports assigned to the specified Server
- */
- public static String getPorts(Server server) {
- StringBuilder ports = new StringBuilder();
- for(Connector c : server.getConnectors()) {
- if (ports.length() > 0) {
- ports.append(", ");
- }
-
- ports.append(((ServerConnector)c).getLocalPort());
- }
-
- return ports.toString();
- }
-
/**
* Thread safe method for setting a fine status message. Messages are suppressed unless "Show all
* notifications" is checked.
diff --git a/src/qz/installer/Installer.java b/src/qz/installer/Installer.java
index 47f850608..13741d724 100644
--- a/src/qz/installer/Installer.java
+++ b/src/qz/installer/Installer.java
@@ -19,13 +19,8 @@
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.io.*;
+import java.nio.file.*;
import java.security.cert.X509Certificate;
import java.util.*;
@@ -43,6 +38,7 @@ public abstract class Installer {
// Silence prompts within our control
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
+ public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
public enum PrivilegeLevel {
USER,
@@ -63,12 +59,15 @@ public enum PrivilegeLevel {
public static Installer getInstance() {
if(instance == null) {
- if(SystemUtilities.isWindows()) {
- instance = new WindowsInstaller();
- } else if(SystemUtilities.isMac()) {
- instance = new MacInstaller();
- } else {
- instance = new LinuxInstaller();
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ instance = new WindowsInstaller();
+ break;
+ case MAC:
+ instance = new MacInstaller();
+ break;
+ default:
+ instance = new LinuxInstaller();
}
}
return instance;
@@ -84,6 +83,9 @@ public static void install(String destination, boolean silent) throws Exception
}
public static boolean preinstall() {
+ getInstance();
+ log.info("Fixing runtime permissions...");
+ instance.setJrePermissions(SystemUtilities.getAppPath().toString());
log.info("Stopping running instances...");
return TaskKiller.killAll();
}
@@ -112,27 +114,44 @@ public static void uninstall() {
}
public Installer deployApp() throws IOException {
- Path src = SystemUtilities.detectAppPath();
+ Path src = SystemUtilities.getAppPath();
Path dest = Paths.get(getDestination());
if(!Files.exists(dest)) {
Files.createDirectories(dest);
}
+ // Delete the JDK blindly
+ FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
FileUtils.copyDirectory(src.toFile(), dest.toFile());
FileUtilities.setPermissionsRecursively(dest, false);
- if(SystemUtilities.isWindows()) {
- // skip
- } else if(SystemUtilities.isMac()) {
- setExecutable("uninstall");
- setExecutable("Contents/MacOS/" + ABOUT_TITLE);
- } else {
- setExecutable("uninstall");
- setExecutable(PROPS_FILE);
+
+ if(!SystemUtilities.isWindows()) {
+ setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
+ setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
+ return setJrePermissions(getDestination());
}
return this;
}
+ private Installer setJrePermissions(String dest) {
+ File jreLocation = new File(dest, JRE_LOCATION);
+ File jreBin = new File(jreLocation, "bin");
+ File jreLib = new File(jreLocation, "lib");
+
+ // Set jre/bin/java and friends executable
+ File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
+ if(files != null) {
+ for(File file : files) {
+ file.setExecutable(true, false);
+ }
+ }
+
+ // Set jspawnhelper executable
+ new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
+ return this;
+ }
+
private void setExecutable(String relativePath) {
new File(getDestination(), relativePath).setExecutable(true, false);
}
@@ -151,16 +170,44 @@ public Installer removeLibs() {
}
public Installer removeLegacyFiles() {
- String[] dirs = { "demo/js/3rdparty", "utils", "auth" };
- String[] files = { "demo/js/qz-websocket.js", "windows-icon.ico", "Contents/Resources/apple-icon.icns" };
- for (String dir : dirs) {
+ ArrayList dirs = new ArrayList<>();
+ ArrayList files = new ArrayList<>();
+ HashMap move = new HashMap<>();
+
+ // QZ Tray 2.0 files
+ dirs.add("demo/js/3rdparty");
+ dirs.add("utils");
+ dirs.add("auth");
+ files.add("demo/js/qz-websocket.js");
+ files.add("windows-icon.ico");
+
+ // QZ Tray 2.1 files
+ if(SystemUtilities.isMac()) {
+ // Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
+ dirs.add("demo");
+ dirs.add("libs");
+ files.add(PROPS_FILE + ".jar");
+ files.add("LICENSE.txt");
+ files.add("uninstall");
+ move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
+ }
+
+ dirs.forEach(dir -> {
try {
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
} catch(IOException ignore) {}
- }
- for (String file : files) {
+ });
+
+ files.forEach(file -> {
new File(instance.getDestination() + File.separator + file).delete();
- }
+ });
+
+ move.forEach((src, dest) -> {
+ try {
+ FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
+ new File(instance.getDestination() + File.separator + dest));
+ } catch(IOException ignore) {}
+ });
return this;
}
@@ -202,8 +249,11 @@ public CertificateManager certGen(boolean forceNew, String... hostNames) throws
if (forceNew || needsInstall) {
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
- List matchingCerts = installer.find();
- installer.remove(matchingCerts);
+ // Skip if running from IDE, this may accidentally remove sandboxed certs
+ if(SystemUtilities.isJar()) {
+ List matchingCerts = installer.find();
+ installer.remove(matchingCerts);
+ }
installer.install(caCert);
FirefoxCertificateInstaller.install(caCert, hostNames);
} else {
@@ -240,8 +290,8 @@ public void removeCerts() {
*/
public Installer addUserSettings() {
// Check for whitelisted certificates in /whitelist/
- Path whiteList = Paths.get(FileUtilities.getParentDirectory(SystemUtilities.getJarPath()), WHITELIST_CERT_DIR);
- if(whiteList.toFile().exists() && whiteList.toFile().isDirectory()) {
+ Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
+ if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
for(File file : whiteList.toFile().listFiles()) {
try {
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
diff --git a/src/qz/installer/LinuxInstaller.java b/src/qz/installer/LinuxInstaller.java
index 0499c85e7..7d58ae9e9 100644
--- a/src/qz/installer/LinuxInstaller.java
+++ b/src/qz/installer/LinuxInstaller.java
@@ -6,6 +6,8 @@
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
+import qz.utils.UnixUtilities;
+
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -87,7 +89,7 @@ public Installer removeLegacyStartup() {
public Installer addSystemSettings() {
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
- if(SystemUtilities.isUbuntu()) {
+ if(UnixUtilities.isUbuntu()) {
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
@@ -166,13 +168,13 @@ public void spawn(List args) throws Exception {
ShellUtilities.execute(args.toArray(new String[args.size()]));
return;
}
- String whoami = ShellUtilities.executeRaw("logname").trim();
- if(whoami.isEmpty() || SystemUtilities.isSolaris()) {
- whoami = System.getenv("SUDO_USER");
+ String sudoer = ShellUtilities.executeRaw("logname").trim();
+ if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
+ sudoer = System.getenv("SUDO_USER");
}
- if(whoami != null && !whoami.trim().isEmpty()) {
- whoami = whoami.trim();
+ if(sudoer != null && !sudoer.trim().isEmpty()) {
+ sudoer = sudoer.trim();
} else {
throw new Exception("Unable to get current user, can't spawn instance");
}
@@ -226,10 +228,10 @@ public void spawn(List args) throws Exception {
}
// Only add vars for the current user
- if(whoami.trim().equals(tempEnv.get("USER"))) {
+ if(sudoer.trim().equals(tempEnv.get("USER"))) {
env.putAll(tempEnv);
} else {
- log.debug("Expected USER={} but got USER={}, skipping results for {}", whoami, tempEnv.get("USER"), pid);
+ log.debug("Expected USER={} but got USER={}, skipping results for {}", sudoer, tempEnv.get("USER"), pid);
}
// Use gtk theme
@@ -256,8 +258,8 @@ public void spawn(List args) throws Exception {
}
// Determine if this environment likes sudo
- String[] sudoCmd = { "sudo", "-E", "-u", whoami, "nohup" };
- String[] suCmd = { "su", whoami, "-c", "nohup" };
+ String[] sudoCmd = { "sudo", "-E", "-u", sudoer, "nohup" };
+ String[] suCmd = { "su", sudoer, "-c", "nohup" };
ArrayList argsList = new ArrayList<>();
if(ShellUtilities.execute("which", "sudo")) {
diff --git a/src/qz/installer/MacInstaller.java b/src/qz/installer/MacInstaller.java
index 070523039..54651038b 100644
--- a/src/qz/installer/MacInstaller.java
+++ b/src/qz/installer/MacInstaller.java
@@ -9,6 +9,7 @@
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
+import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,6 +19,7 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.*;
import java.util.HashMap;
import java.util.List;
@@ -46,7 +48,7 @@ public Installer addStartupEntry() {
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
// Disable service until reboot
if(SystemUtilities.isMac()) {
- ShellUtilities.execute("launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
+ ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
} catch(IOException e) {
log.warn("Unable to write startup file: {}", dest, e);
@@ -84,13 +86,8 @@ public Installer removeSystemSettings() {
public Installer removeLegacyStartup() {
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
String script = "tell application \"System Events\" to delete "
- + "every login item where name is \"" + ABOUT_TITLE + "\"";
-
- // Handle edge-case for when running from IDE
- File jar = new File(SystemUtilities.getJarPath());
- if(jar.getName().endsWith(".jar")) {
- script += " or name is \"" + jar.getName() + "\"";
- }
+ + "every login item where name is \"" + ABOUT_TITLE + "\""
+ + " or name is \"" + PROPS_FILE + ".jar\"";
// Run on background thread in case System Events is hung or slow to respond
final String finalScript = script;
@@ -100,17 +97,6 @@ public Installer removeLegacyStartup() {
return this;
}
- public static String getAppPath() {
- // Return the Mac ".app" location
- String target = SystemUtilities.getJarPath();
- int appIndex = target.indexOf(".app/");
- if (appIndex > 0) {
- return target.substring(0, appIndex) + ".app";
- }
- // Fallback on the ".jar" location
- return target;
- }
-
public static String getPackageName() {
String packageName;
String[] parts = ABOUT_URL.split("\\W");
@@ -127,13 +113,13 @@ public static String getPackageName() {
public void spawn(List args) throws Exception {
if(SystemUtilities.isAdmin()) {
// macOS unconventionally uses "$USER" during its install process
- String whoami = System.getenv("USER");
- if(whoami == null || whoami.isEmpty() || whoami.equals("root")) {
+ String sudoer = System.getenv("USER");
+ if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
// Fallback, should only fire via Terminal + sudo
- whoami = ShellUtilities.executeRaw("logname").trim();
+ sudoer = ShellUtilities.executeRaw("logname").trim();
}
// Start directly without waitFor(...), avoids deadlocking
- Runtime.getRuntime().exec(new String[] { "su", whoami, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
+ Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
} else {
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
}
diff --git a/src/qz/installer/TaskKiller.java b/src/qz/installer/TaskKiller.java
index 80fab2973..3847b0d3e 100644
--- a/src/qz/installer/TaskKiller.java
+++ b/src/qz/installer/TaskKiller.java
@@ -1,13 +1,21 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
package qz.installer;
-import com.sun.jna.platform.win32.Kernel32;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.installer.certificate.firefox.locator.AppLocator;
-import qz.utils.MacUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
+import qz.utils.WindowsUtilities;
import qz.ws.PrintSocketServer;
import java.io.File;
@@ -37,7 +45,7 @@ public static boolean killAll() {
String[] killCmd;
// Disable service until reboot
if(SystemUtilities.isMac()) {
- ShellUtilities.execute("launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
+ ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
if(SystemUtilities.isWindows()) {
// Windows may be running under javaw.exe (normal) or java.exe (terminal)
@@ -80,7 +88,7 @@ public static boolean killAll() {
}
}
- if(SystemUtilities.isWindowsXP()) {
+ if(WindowsUtilities.isWindowsXP()) {
File f = new File("TempWmicBatchFile.bat");
if(f.exists()) {
f.deleteOnExit();
diff --git a/src/qz/installer/WindowsSpecialFolders.java b/src/qz/installer/WindowsSpecialFolders.java
index dc042628e..362c1957f 100644
--- a/src/qz/installer/WindowsSpecialFolders.java
+++ b/src/qz/installer/WindowsSpecialFolders.java
@@ -11,7 +11,7 @@
package qz.installer;
import com.sun.jna.platform.win32.*;
-import qz.utils.SystemUtilities;
+import qz.utils.WindowsUtilities;
/**
* Windows XP-compatible special folder's wrapper for JNA
@@ -84,7 +84,7 @@ public enum WindowsSpecialFolders {
}
public String getPath() {
- if(SystemUtilities.isWindowsXP()) {
+ if(WindowsUtilities.isWindowsXP()) {
return Shell32Util.getSpecialFolderPath(csidl, false);
}
return Shell32Util.getKnownFolderPath(guid);
diff --git a/src/qz/installer/certificate/CertificateManager.java b/src/qz/installer/certificate/CertificateManager.java
index 3094b0ae8..05da427ca 100644
--- a/src/qz/installer/certificate/CertificateManager.java
+++ b/src/qz/installer/certificate/CertificateManager.java
@@ -25,6 +25,7 @@
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.FileUtilities;
+import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import java.io.*;
@@ -336,9 +337,18 @@ public static File getWritableLocation(String ... subDirs) throws IOException {
// Get an array of preferred directories
ArrayList locs = new ArrayList<>();
- if (subDirs.length == 0) {
+ // Sandbox is only supported on macOS currently
+ boolean sandboxed = false;
+ if(SystemUtilities.isMac()) {
+ sandboxed = MacUtilities.isSandboxed();
+ //todo move to about security table or delete
+ log.debug("Running in a sandbox: {}", sandboxed);
+ }
+
+ // Sandboxed installations must remain sealed, don't write to them
+ if (subDirs.length == 0 && !sandboxed) {
// Assume root directory is next to jar (e.g. qz-tray.properties)
- Path appPath = SystemUtilities.detectAppPath();
+ Path appPath = SystemUtilities.getJarParentPath();
// Handle null path, such as running from IDE
if(appPath != null) {
locs.add(appPath);
@@ -379,7 +389,7 @@ public static File getWritableLocation(String ... subDirs) throws IOException {
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
log.info("Try to find SSL properties file...");
- Path[] locations = {SystemUtilities.detectAppPath(), SHARED_DIR, USER_DIR};
+ Path[] locations = {SystemUtilities.getJarParentPath(), SHARED_DIR, USER_DIR};
Properties props = null;
for(Path location : locations) {
diff --git a/src/qz/installer/certificate/LinuxCertificateInstaller.java b/src/qz/installer/certificate/LinuxCertificateInstaller.java
index e7f8a0d04..73aad3f8c 100644
--- a/src/qz/installer/certificate/LinuxCertificateInstaller.java
+++ b/src/qz/installer/certificate/LinuxCertificateInstaller.java
@@ -16,6 +16,7 @@
import qz.installer.Installer;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
+import qz.utils.UnixUtilities;
import javax.swing.*;
import java.awt.*;
@@ -116,7 +117,7 @@ public boolean add(File certFile) {
private boolean findCertutil() {
if (!ShellUtilities.execute("which", "certutil")) {
- if (SystemUtilities.isUbuntu() && certType == SYSTEM && promptCertutil()) {
+ if (UnixUtilities.isUbuntu() && certType == SYSTEM && promptCertutil()) {
return ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
} else {
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on browsers which depend on it.");
diff --git a/src/qz/installer/certificate/NativeCertificateInstaller.java b/src/qz/installer/certificate/NativeCertificateInstaller.java
index dcc14c7f9..79966b4cc 100644
--- a/src/qz/installer/certificate/NativeCertificateInstaller.java
+++ b/src/qz/installer/certificate/NativeCertificateInstaller.java
@@ -34,12 +34,16 @@ public static NativeCertificateInstaller getInstance() {
}
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
if (instance == null) {
- if (SystemUtilities.isWindows()) {
- instance = new WindowsCertificateInstaller(type);
- } else if(SystemUtilities.isMac()) {
- instance = new MacCertificateInstaller(type);
- } else {
- instance = new LinuxCertificateInstaller(type);
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ instance = new WindowsCertificateInstaller(type);
+ break;
+ case MAC:
+ instance = new MacCertificateInstaller(type);
+ break;
+ case LINUX:
+ default:
+ instance = new LinuxCertificateInstaller(type);
}
}
return instance;
@@ -69,10 +73,14 @@ public boolean install(X509Certificate cert) {
public boolean install(File certFile) {
String helper = instance.getClass().getSimpleName();
String store = instance.getInstallType().name();
- if (remove(find())) {
- log.info("Certificate removed from {} store using {}", store, helper);
+ if(SystemUtilities.isJar()) {
+ if (remove(find())) {
+ log.info("Certificate removed from {} store using {}", store, helper);
+ } else {
+ log.warn("Could not remove certificate from {} store using {}", store, helper);
+ }
} else {
- log.warn("Could not remove certificate from {} store using {}", store, helper);
+ log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
}
if (add(certFile)) {
log.info("Certificate added to {} store using {}", store, helper);
diff --git a/src/qz/installer/certificate/WindowsCertificateInstallerCli.java b/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
index 66c27de5b..a134b0e13 100644
--- a/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
+++ b/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
@@ -15,7 +15,7 @@
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ShellUtilities;
-import qz.utils.SystemUtilities;
+import qz.utils.WindowsUtilities;
import java.io.*;
import java.util.ArrayList;
@@ -35,7 +35,7 @@ public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
}
public boolean add(File certFile) {
- if (SystemUtilities.isWindowsXP()) return false;
+ if (WindowsUtilities.isWindowsXP()) return false;
if (certType == Installer.PrivilegeLevel.USER) {
// This will prompt the user
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
@@ -45,7 +45,7 @@ public boolean add(File certFile) {
}
public boolean remove(List idList) {
- if (SystemUtilities.isWindowsXP()) return false;
+ if (WindowsUtilities.isWindowsXP()) return false;
boolean success = true;
for (String certId : idList) {
if (certType == Installer.PrivilegeLevel.USER) {
diff --git a/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java
index da7cad9fb..63852bb7c 100644
--- a/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java
+++ b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java
@@ -136,7 +136,7 @@ private static void writeParsedConfig(AppInfo appInfo, String certData, boolean
fieldMap.put("%CERT_DATA%", certData);
fieldMap.put("%COMMON_NAME%", hostNames[0]);
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
- fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.detectAppPath() != null ? SystemUtilities.detectAppPath().toString() : "" : "");
+ fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
fieldMap.put("%UNINSTALL%", "" + uninstall);
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
diff --git a/src/qz/installer/certificate/firefox/locator/AppLocator.java b/src/qz/installer/certificate/firefox/locator/AppLocator.java
index 026c1f947..025c89c90 100644
--- a/src/qz/installer/certificate/firefox/locator/AppLocator.java
+++ b/src/qz/installer/certificate/firefox/locator/AppLocator.java
@@ -72,11 +72,13 @@ public static AppLocator getInstance() {
}
private static AppLocator getPlatformSpecificAppLocator() {
- if (SystemUtilities.isWindows()) {
- return new WindowsAppLocator();
- } else if (SystemUtilities.isMac()) {
- return new MacAppLocator();
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ return new WindowsAppLocator();
+ case MAC:
+ return new MacAppLocator();
+ default:
+ return new LinuxAppLocator();
}
- return new LinuxAppLocator();
}
}
\ No newline at end of file
diff --git a/src/qz/installer/certificate/firefox/locator/MacAppLocator.java b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
index 641b8cfbd..d14201f92 100644
--- a/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
+++ b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
@@ -124,7 +124,7 @@ public ArrayList getPidPaths(ArrayList pids) {
* Calculate executable path by parsing Contents/Info.plist
*/
private static Path getExePath(String appPath) {
- Path path = Paths.get(appPath);
+ Path path = Paths.get(appPath).toAbsolutePath().normalize();
Path plist = path.resolve("Contents/Info.plist");
Document doc;
try {
@@ -133,7 +133,7 @@ private static Path getExePath(String appPath) {
return null;
}
// Convert potentially binary plist files to XML
- Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toAbsolutePath().toString(), "-o", "-"}, ShellUtilities.envp);
+ Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
} catch(IOException | ParserConfigurationException | SAXException e) {
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
diff --git a/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java
index 793a4a30f..972856720 100644
--- a/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java
+++ b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java
@@ -76,7 +76,7 @@ public ArrayList getPids(ArrayList processNames) {
}
}
- if(SystemUtilities.isWindowsXP()) {
+ if(WindowsUtilities.isWindowsXP()) {
// Cleanup XP crumbs per https://stackoverflow.com/q/12391655/3196753
File f = new File("TempWmicBatchFile.bat");
if(f.exists()) {
diff --git a/src/qz/installer/shortcut/MacShortcutCreator.java b/src/qz/installer/shortcut/MacShortcutCreator.java
index 2290034a6..7ca89d488 100644
--- a/src/qz/installer/shortcut/MacShortcutCreator.java
+++ b/src/qz/installer/shortcut/MacShortcutCreator.java
@@ -16,8 +16,8 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.common.Constants;
-import qz.installer.MacInstaller;
import qz.utils.MacUtilities;
+import qz.utils.SystemUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -85,7 +85,14 @@ public boolean canAutoStart() {
public void createDesktopShortcut() {
try {
new File(SHORTCUT_PATH).delete();
- Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), Paths.get(MacInstaller.getAppPath()));
+ if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
+ // We're probably running from an .app bundle
+ Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
+ } else {
+ // We're running from a mystery location, use the jar instead
+ Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
+ }
+
} catch(IOException e) {
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
}
diff --git a/src/qz/installer/shortcut/WindowsShortcutCreator.java b/src/qz/installer/shortcut/WindowsShortcutCreator.java
index 18f5090ef..e9f2a2143 100644
--- a/src/qz/installer/shortcut/WindowsShortcutCreator.java
+++ b/src/qz/installer/shortcut/WindowsShortcutCreator.java
@@ -45,8 +45,9 @@ private void createShortcut(String folderPath) {
/**
* Calculates .exe path from .jar
+ * fixme: overlaps SystemUtilities.getAppPath
*/
private static String getAppPath() {
- return SystemUtilities.getJarPath().replaceAll(".jar$", ".exe");
+ return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
}
}
diff --git a/src/qz/printer/PrintOptions.java b/src/qz/printer/PrintOptions.java
index 40dc9e7d1..c321d82cd 100644
--- a/src/qz/printer/PrintOptions.java
+++ b/src/qz/printer/PrintOptions.java
@@ -39,16 +39,17 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities
if (configOpts == null) { return; }
//check for raw options
- if (!configOpts.isNull("altPrinting")) {
- try {
- rawOptions.altPrinting = configOpts.getBoolean("altPrinting");
- if (rawOptions.altPrinting && SystemUtilities.isWindows()) {
- log.warn("Alternate raw printing is not supported on Windows");
- rawOptions.altPrinting = false;
- }
- }
- catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "altPrinting", configOpts.opt("altPrinting")); }
+ if (!configOpts.isNull("forceRaw")) {
+ rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false);
+ } else if (!configOpts.isNull("altPrinting")) {
+ log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead.");
+ rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false);
+ }
+ if (rawOptions.forceRaw && SystemUtilities.isWindows()) {
+ log.warn("Forced raw printing is not supported on Windows");
+ rawOptions.forceRaw = false;
}
+
if (!configOpts.isNull("encoding")) {
JSONObject encodings = configOpts.optJSONObject("encoding");
if (encodings != null) {
@@ -90,6 +91,10 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities
if (!configOpts.isNull("jobName")) {
rawOptions.jobName = configOpts.optString("jobName", null);
}
+ if (!configOpts.isNull("retainTemp")) {
+ rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false);
+ }
+
//check for pixel options
if (!configOpts.isNull("units")) {
@@ -406,17 +411,18 @@ public Pixel getPixelOptions() {
/** Raw printing options */
public class Raw {
- private boolean altPrinting = false; //Alternate printing for linux systems
+ private boolean forceRaw = false; //Alternate printing for linux systems
private String destEncoding = null; //Text encoding / charset
private String srcEncoding = null; //Conversion text encoding
private String spoolEnd = null; //End of document character(s)
private int spoolSize = 1; //Pages per spool
private int copies = 1; //Job copies
private String jobName = null; //Job name
+ private boolean retainTemp = false; //Retain any temporary files
- public boolean isAltPrinting() {
- return altPrinting;
+ public boolean isForceRaw() {
+ return forceRaw;
}
public String getDestEncoding() {
@@ -439,6 +445,8 @@ public int getCopies() {
return copies;
}
+ public boolean isRetainTemp() { return retainTemp; }
+
public String getJobName(String defaultVal) {
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
}
diff --git a/src/qz/printer/action/PrintRaw.java b/src/qz/printer/action/PrintRaw.java
index 9d8e6ad40..c94cbfb1a 100644
--- a/src/qz/printer/action/PrintRaw.java
+++ b/src/qz/printer/action/PrintRaw.java
@@ -29,6 +29,7 @@
import qz.printer.PrintOptions;
import qz.printer.PrintOutput;
import qz.printer.info.NativePrinter;
+import qz.printer.status.CupsUtils;
import qz.utils.*;
import javax.imageio.ImageIO;
@@ -62,6 +63,11 @@ public class PrintRaw implements PrintProcessor {
private String destEncoding = null;
+ private enum Backend {
+ CUPS_RSS,
+ CUPS_LPR,
+ WIN32_WMI
+ }
public PrintRaw() {
commands = new ByteArrayBuilder();
@@ -314,8 +320,14 @@ public void print(PrintOutput output, PrintOptions options) throws PrintExceptio
} else if (output.isSetFile()) {
printToFile(output.getFile(), bab.getByteArray());
} else {
- if (rawOpts.isAltPrinting()) {
- printToAlternate(output.getNativePrinter(), bab.getByteArray());
+ if (rawOpts.isForceRaw()) {
+ if(SystemUtilities.isWindows()) {
+ // Placeholder only; not yet supported
+ printToBackend(output.getNativePrinter(), bab.getByteArray(), rawOpts.isRetainTemp(), Backend.WIN32_WMI);
+ } else {
+ // Try CUPS backend first, fallback to LPR
+ printToBackend(output.getNativePrinter(), bab.getByteArray(), rawOpts.isRetainTemp(), Backend.CUPS_RSS, Backend.CUPS_LPR);
+ }
} else {
printToPrinter(output.getPrintService(), bab.getByteArray(), rawOpts);
}
@@ -425,25 +437,45 @@ public void printJobRequiresAttention(PrintJobEvent printJobEvent) {
}
/**
- * Alternate printing mode for CUPS capable OSs, issues lp via command line
- * on Linux, BSD, Solaris, OSX, etc. This will never work on Windows.
+ * Direct/backend printing modes for forced raw printing
*/
- public void printToAlternate(NativePrinter printer, byte[] cmds) throws IOException, PrintException {
+ public void printToBackend(NativePrinter printer, byte[] cmds, boolean retainTemp, Backend... backends) throws IOException, PrintException {
File tmp = File.createTempFile("qz_raw_", null);
+ boolean success = false;
try {
printToFile(tmp, cmds);
- String[] lpCmd = new String[] {
- "lp", "-d", printer.getPrinterId(), "-o", "raw", tmp.getAbsolutePath()
- };
- boolean success = ShellUtilities.execute(lpCmd);
-
+ for(Backend backend : backends) {
+ switch(backend) {
+ case CUPS_LPR:
+ // Use command line "lp" on Linux, BSD, Solaris, OSX, etc.
+ String[] lpCmd = new String[] {"lp", "-d", printer.getPrinterId(), "-o", "raw", tmp.getAbsolutePath()};
+ if (!(success = ShellUtilities.execute(lpCmd))) {
+ log.debug(StringUtils.join(lpCmd, ' '));
+ }
+ break;
+ case CUPS_RSS:
+ // Submit job via cupsDoRequest(...) via JNA against localhost:631\
+ success = CupsUtils.sendRawFile(printer, tmp);
+ break;
+ case WIN32_WMI:
+ default:
+ throw new UnsupportedOperationException("Raw backend \"" + backend + "\" is not yet supported.");
+ }
+ if(success) {
+ break;
+ }
+ }
if (!success) {
- throw new PrintException("Alternate printing failed: " + StringUtils.join(lpCmd, ' '));
+ throw new PrintException("Forced raw printing failed");
}
}
finally {
- if (!tmp.delete()) {
- tmp.deleteOnExit();
+ if(!retainTemp) {
+ if (!tmp.delete()) {
+ tmp.deleteOnExit();
+ }
+ } else{
+ log.warn("Temp file retained: {}", tmp);
}
}
}
diff --git a/src/qz/printer/action/WebApp.java b/src/qz/printer/action/WebApp.java
index 8472c1a1f..405ed61b4 100644
--- a/src/qz/printer/action/WebApp.java
+++ b/src/qz/printer/action/WebApp.java
@@ -25,14 +25,13 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import qz.common.Constants;
+import qz.utils.LibUtilities;
import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
import java.awt.image.BufferedImage;
-import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
-import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -158,19 +157,7 @@ public static synchronized void initialize() throws IOException {
headless = false;
// JDK11+ depends bundled javafx
- if (Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("11.0.0"))) {
- // JavaFX native libs
- if (SystemUtilities.isJar()) {
- SystemUtilities.insertPathProperty(
- "java.library.path",
- new File(SystemUtilities.detectJarPath()).getParent() + "/libs/",
- "/jni" /* appends to end if not found */
- );
- } else if (hasConflictingLib()) {
- // IDE helper for "no suitable pipeline found" errors
- System.err.println("\n=== WARNING ===\nWrong javafx platform detected. Delete lib/javafx/ to correct this.\n");
- }
-
+ if (Constants.JAVA_VERSION.getMajorVersion() >= 11) {
// Monocle default for unit tests
boolean useMonocle = true;
if (PrintSocketServer.getTrayManager() != null) {
@@ -186,8 +173,13 @@ public static synchronized void initialize() throws IOException {
log.trace("Initializing monocle platform");
System.setProperty("javafx.platform", "monocle");
// Don't set glass.platform on Linux per https://github.com/qzind/tray/issues/702
- if ((SystemUtilities.isWindows() || SystemUtilities.isMac())) {
- System.setProperty("glass.platform", "Monocle");
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ case MAC:
+ System.setProperty("glass.platform", "Monocle");
+ break;
+ default:
+ // don't set "glass.platform"
}
//software rendering required headless environments
@@ -478,19 +470,6 @@ public static void unlatch(Throwable t) {
stage.hide();
}
- public static boolean hasConflictingLib() {
- // If running from the IDE, make sure we're not using the wrong libs
- URL url = Application.class.getResource("/" + Application.class.getName().replace('.', '/') + ".class");
- String graphicsJar = url.toString().replaceAll("file:/|jar:", "").replaceAll("!.*", "");
- log.trace("JavaFX will startup using {}", graphicsJar);
- if (SystemUtilities.isWindows()) {
- return !graphicsJar.contains("windows");
- } else if (SystemUtilities.isMac()) {
- return !graphicsJar.contains("osx") && !graphicsJar.contains("mac");
- }
- return !graphicsJar.contains("linux");
- }
-
public static Version getWebkitVersion() {
if(webkitVersion == null) {
if(webView != null) {
diff --git a/src/qz/printer/info/NativePrinterMap.java b/src/qz/printer/info/NativePrinterMap.java
index ec4997c9a..be85b9a0a 100644
--- a/src/qz/printer/info/NativePrinterMap.java
+++ b/src/qz/printer/info/NativePrinterMap.java
@@ -2,11 +2,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import qz.printer.PrintServiceMatcher;
import qz.utils.SystemUtilities;
import javax.print.PrintService;
-import javax.print.attribute.standard.PrinterName;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@@ -22,10 +20,12 @@ public abstract class NativePrinterMap extends ConcurrentHashMap getAllStatuses() {
@@ -62,7 +107,8 @@ public static ArrayList getAllStatuses() {
Pointer request = cups.ippNewRequest(IPP.GET_PRINTERS);
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
- Pointer response = cups.cupsDoRequest(http, request, "/");
+
+ Pointer response = doRequest(request, "/");
Pointer stateAttr = cups.ippFindAttribute(response, "printer-state", IPP.TAG_ENUM);
Pointer reasonAttr = cups.ippFindAttribute(response, "printer-state-reasons", IPP.TAG_KEYWORD);
Pointer nameAttr = cups.ippFindAttribute(response, "printer-name", IPP.TAG_NAME);
@@ -135,7 +181,7 @@ static void startSubscription(int rssPort) {
new StringArray(subscriptions));
cups.ippAddInteger(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_INTEGER, "notify-lease-duration", 0);
- Pointer response = cups.cupsDoRequest(http, request, "/");
+ Pointer response = doRequest(request, "/");
Pointer attr = cups.ippFindAttribute(response, "notify-subscription-id", IPP.TAG_INTEGER);
if (attr != Pointer.NULL) { subscriptionID = cups.ippGetInteger(attr, 0); }
@@ -155,16 +201,55 @@ static void endSubscription(int id) {
URIUtil.encodePath("ipp://localhost:" + IPP.PORT));
cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-subscription-id", id);
- Pointer response = cups.cupsDoRequest(http, request, "/");
+ Pointer response = doRequest(request, "/");
cups.ippDelete(response);
}
public synchronized static void freeIppObjs() {
- if (httpInitialised) {
- httpInitialised = false;
+ if (http != null) {
endSubscription(subscriptionID);
subscriptionID = IPP.INT_UNDEFINED;
cups.httpClose(http);
+ http = null;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ static void parseResponse(Pointer response) {
+ Pointer attr = Cups.INSTANCE.ippFirstAttribute(response);
+ while (true) {
+ if (attr == Pointer.NULL) {
+ break;
+ }
+ System.out.println(parseAttr(attr));
+ attr = Cups.INSTANCE.ippNextAttribute(response);
+ }
+ System.out.println("------------------------");
+ }
+
+ static String parseAttr(Pointer attr){
+ int valueTag = Cups.INSTANCE.ippGetValueTag(attr);
+ int attrCount = Cups.INSTANCE.ippGetCount(attr);
+ String data = "";
+ String attrName = Cups.INSTANCE.ippGetName(attr);
+ for (int i = 0; i < attrCount; i++) {
+ if (valueTag == Cups.INSTANCE.ippTagValue("Integer")) {
+ data += Cups.INSTANCE.ippGetInteger(attr, i);
+ } else if (valueTag == Cups.INSTANCE.ippTagValue("Boolean")) {
+ data += (Cups.INSTANCE.ippGetInteger(attr, i) == 1);
+ } else if (valueTag == Cups.INSTANCE.ippTagValue("Enum")) {
+ data += Cups.INSTANCE.ippEnumString(attrName, Cups.INSTANCE.ippGetInteger(attr, i));
+ } else {
+ data += Cups.INSTANCE.ippGetString(attr, i, "");
+ }
+ if (i + 1 < attrCount) {
+ data += ", ";
+ }
+ }
+
+ if (attrName == null){
+ return "------------------------";
}
+ return String.format("%s: %d %s {%s}", attrName, attrCount, Cups.INSTANCE.ippTagString(valueTag), data);
}
}
diff --git a/src/qz/ui/AboutDialog.java b/src/qz/ui/AboutDialog.java
index 5a48680cf..3aefd9a9f 100644
--- a/src/qz/ui/AboutDialog.java
+++ b/src/qz/ui/AboutDialog.java
@@ -62,7 +62,7 @@ public void initComponents() {
infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS));
LinkLabel linkLibrary = new LinkLabel("Detailed library information");
- if(server.isRunning() && !server.isStopping()) {
+ if(server != null && server.isRunning() && !server.isStopping()) {
linkLibrary.setLinkLocation(String.format("%s://%s:%s", server.getURI().getScheme(), AboutInfo.getPreferredHostname(), server.getURI().getPort()));
}
Box versionBox = Box.createHorizontalBox();
diff --git a/src/qz/ui/BasicDialog.java b/src/qz/ui/BasicDialog.java
index bb2df0fc0..40829d070 100644
--- a/src/qz/ui/BasicDialog.java
+++ b/src/qz/ui/BasicDialog.java
@@ -175,9 +175,7 @@ public ImageIcon getIcon(IconCache.Icon icon) {
public void setVisible(boolean b) {
// fix window focus on macOS
if (SystemUtilities.isMac() && !GraphicsEnvironment.isHeadless()) {
- ShellUtilities.executeAppleScript("tell application \"System Events\" \n" +
- "set frontmost of every process whose unix id is " + SystemUtilities.getProcessId() + " to true \n" +
- "end tell");
+ MacUtilities.setFocus();
}
super.setVisible(b);
}
diff --git a/src/qz/ui/LogDialog.java b/src/qz/ui/LogDialog.java
index 4f72fb989..33090b628 100644
--- a/src/qz/ui/LogDialog.java
+++ b/src/qz/ui/LogDialog.java
@@ -6,14 +6,12 @@
import qz.ui.component.IconCache;
import qz.ui.component.LinkLabel;
import qz.utils.FileUtilities;
-import qz.utils.SystemUtilities;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
-import java.io.IOException;
import java.io.OutputStream;
/**
diff --git a/src/qz/ui/SiteManagerDialog.java b/src/qz/ui/SiteManagerDialog.java
index 18ca7f7b2..9f27412ba 100644
--- a/src/qz/ui/SiteManagerDialog.java
+++ b/src/qz/ui/SiteManagerDialog.java
@@ -456,7 +456,7 @@ private void addCertificates(File[] certFiles, ContainerList
}
private void showInvalidCertWarning(File file, Certificate cert) {
- String override = FileUtilities.getParentDirectory(SystemUtilities.getJarPath()) + File.separator + Constants.OVERRIDE_CERT;
+ Path override = SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT);
String message = String.format(IMPORT_NEEDED,
cert.getCommonName(),
override);
@@ -464,7 +464,7 @@ private void showInvalidCertWarning(File file, Certificate cert) {
if(copyAnswer == JOptionPane.YES_OPTION) {
Cursor backupCursor = getCursor();
setCursor(new Cursor(Cursor.WAIT_CURSOR));
- boolean copySuccess = ShellUtilities.elevateCopy(file.toPath(), Paths.get(SystemUtilities.getJarPath()).getParent().resolve(Constants.OVERRIDE_CERT));
+ boolean copySuccess = ShellUtilities.elevateCopy(file.toPath(), SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT));
setCursor(backupCursor);
if(copySuccess) {
Certificate.scanAdditionalCAs();
diff --git a/src/qz/ui/component/IconCache.java b/src/qz/ui/component/IconCache.java
index 9bce86016..df35f5b1d 100644
--- a/src/qz/ui/component/IconCache.java
+++ b/src/qz/ui/component/IconCache.java
@@ -16,6 +16,7 @@
import qz.utils.ColorUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UbuntuUtilities;
+import qz.utils.UnixUtilities;
import javax.imageio.ImageIO;
import javax.swing.*;
@@ -258,7 +259,7 @@ public void setBgColor(Icon i, Color bgColor) {
*/
public void fixTrayIcons(boolean darkTaskbar) {
// Fix the tray icon to look proper on Ubuntu
- if (SystemUtilities.isUbuntu()) {
+ if (UnixUtilities.isUbuntu()) {
UbuntuUtilities.fixTrayIcons(this);
}
diff --git a/src/qz/ui/component/LinkLabel.java b/src/qz/ui/component/LinkLabel.java
index 987ab3134..0b752f5a6 100644
--- a/src/qz/ui/component/LinkLabel.java
+++ b/src/qz/ui/component/LinkLabel.java
@@ -6,7 +6,6 @@
import qz.ui.Themeable;
import qz.utils.ShellUtilities;
-import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -14,7 +13,6 @@
import java.awt.event.MouseListener;
import java.awt.font.TextAttribute;
import java.io.File;
-import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
diff --git a/src/qz/ui/tray/TaskbarTrayIcon.java b/src/qz/ui/tray/TaskbarTrayIcon.java
index c1755c8b4..e0d56bbef 100644
--- a/src/qz/ui/tray/TaskbarTrayIcon.java
+++ b/src/qz/ui/tray/TaskbarTrayIcon.java
@@ -1,8 +1,8 @@
package qz.ui.tray;
import qz.common.Constants;
-import qz.utils.SystemUtilities;
import qz.utils.UbuntuUtilities;
+import qz.utils.UnixUtilities;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
@@ -27,7 +27,7 @@ private void initializeComponents(Image trayImage, final ActionListener exitList
setTaskBarTitle(getTitle());
setSize(0, 0);
getContentPane().setBackground(Color.BLACK);
- if (SystemUtilities.isUbuntu()) {
+ if (UnixUtilities.isUbuntu()) {
// attempt to camouflage the single pixel left behind
getContentPane().setBackground(UbuntuUtilities.getTrayColor());
}
diff --git a/src/qz/utils/ArgParser.java b/src/qz/utils/ArgParser.java
index 688748e27..32101d6e6 100644
--- a/src/qz/utils/ArgParser.java
+++ b/src/qz/utils/ArgParser.java
@@ -13,6 +13,7 @@
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import qz.build.JLink;
import qz.common.Constants;
import qz.common.SecurityInfo;
import qz.exception.MissingArgException;
@@ -45,11 +46,13 @@ public int getCode() {
}
protected static final Logger log = LoggerFactory.getLogger(ArgParser.class);
+
private static final String USAGE_COMMAND = String.format("java -jar %s.jar", PROPS_FILE);
private static final int DESCRIPTION_COLUMN = 30;
private static final int INDENT_SIZE = 2;
private List args;
+ private boolean headless;
private ExitStatus exitStatus;
public ArgParser(String[] args) {
@@ -60,14 +63,12 @@ public List getArgs() {
return args;
}
- public ExitStatus getExitStatus() {
- return exitStatus;
- }
-
public int getExitCode() {
return exitStatus.getCode();
}
+ public boolean isHeadless() { return headless; };
+
/**
* Gets the requested flag status
*/
@@ -197,6 +198,24 @@ public ExitStatus processInstallerArgs(ArgValue argValue, List args) {
}
}
+ public ExitStatus processBuildArgs(ArgValue argValue) {
+ try {
+ switch(argValue) {
+ case JLINK:
+ new JLink(valueOf("--platform", "-p"), valueOf("--arch", "-a"), valueOf("--gc", "-g"));
+ return SUCCESS;
+ default:
+ throw new UnsupportedOperationException("Build type " + argValue + " is not yet supported");
+ }
+ } catch(MissingArgException e) {
+ log.error("Valid usage:\n {} {}", USAGE_COMMAND, argValue.getUsage());
+ return USAGE_ERROR;
+ } catch(Exception e) {
+ log.error("Build step {} failed", argValue, e);
+ return GENERAL_ERROR;
+ }
+ }
+
/**
* Attempts to intercept utility command line args.
* If intercepted, returns true and sets the exitStatus
to a usable integer
@@ -223,7 +242,7 @@ public boolean intercept() {
}
} else {
// Show generic help
- for(ArgType argType : ArgType.values()) {
+ for(ArgValue.ArgType argType : ArgValue.ArgType.values()) {
System.out.println(String.format("%s%s", System.lineSeparator(), argType));
for(ArgValue argValue : ArgValue.filter(argType)) {
printHelp(argValue);
@@ -245,11 +264,18 @@ public boolean intercept() {
return true;
}
- // Second, handle installation commands (e.g. install, uninstall, certgen, etc)
- for(ArgValue argValue : ArgValue.filter(ArgType.INSTALLER)) {
- if (hasFlag(argValue)) {
- exitStatus = processInstallerArgs(argValue, args);
- return true;
+ // Second, handle build or install commands
+ ArgValue found = hasFlags(true, ArgValue.filter(ArgType.INSTALLER, ArgType.BUILD));
+ if(found != null) {
+ switch(found.getType()) {
+ case BUILD:
+ // Handle build commands (e.g. jlink)
+ exitStatus = processBuildArgs(found);
+ return true;
+ case INSTALLER:
+ // Handle install commands (e.g. install, uninstall, certgen, etc)
+ exitStatus = processInstallerArgs(found, args);
+ return true;
}
}
@@ -268,6 +294,13 @@ public boolean intercept() {
return false;
}
+ // Handle headless flag
+ if(headless = hasFlag("-h", "--headless")) {
+ // Don't intercept
+ exitStatus = SUCCESS;
+ return false;
+ }
+
// Handle version request
if (hasFlag(ArgValue.VERSION)) {
System.out.println(Constants.VERSION);
diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java
index ef815c7f3..af57125cd 100644
--- a/src/qz/utils/ArgValue.java
+++ b/src/qz/utils/ArgValue.java
@@ -46,7 +46,11 @@ public enum ArgValue {
UNINSTALL(INSTALLER, "Perform all uninstall tasks: Stop instances, delete files, unregister settings.", null,
"uninstall"),
SPAWN(INSTALLER, "Spawn an instance of the specified program as the logged-in user, avoiding starting as the root user if possible.", "spawn [program params ...]",
- "spawn");
+ "spawn"),
+
+ // Build stubs
+ JLINK(BUILD, "Download, compress and bundle a Java Runtime", "jlink [--platform mac|windows|linux] [--arch x64|aarch64] [--gc hotspot|j9]",
+ "jlink");
private ArgType argType;
private String description;
@@ -79,7 +83,8 @@ public enum ArgType {
INFORMATION,
ACTION,
OPTION,
- INSTALLER
+ INSTALLER,
+ BUILD,
}
public static ArgValue[] filter(ArgType ... argTypes) {
diff --git a/src/qz/utils/ConnectionUtilities.java b/src/qz/utils/ConnectionUtilities.java
index 0be2602ca..8f5c4dbee 100644
--- a/src/qz/utils/ConnectionUtilities.java
+++ b/src/qz/utils/ConnectionUtilities.java
@@ -151,20 +151,19 @@ public String toString() {
}
private static String getArch() {
- String arch = System.getProperty("os.arch");
- if(arch != null) {
- arch = arch.toLowerCase(Locale.ENGLISH);
- if(arch.startsWith("amd") || arch.startsWith("x86") || arch.startsWith("i386")) {
+ switch(SystemUtilities.getJreArch()) {
+ case X86:
+ case X86_64:
return "x86";
- } else if(arch.startsWith("aarch") || arch.startsWith("arm")) {
+ case AARCH64:
return "arm";
- } else if(arch.startsWith("riscv") || arch.startsWith("rv")) {
+ case RISCV:
return "riscv";
- } if(arch.startsWith("ppc") || arch.startsWith("power")) {
+ case PPC:
return "ppc";
- }
+ default:
+ return "unknown";
}
- return arch == null ? "unknown" : arch;
}
private static String getBitness() {
@@ -173,17 +172,17 @@ private static String getBitness() {
if(bitness != null ) {
return bitness;
}
- // Fallback on parsing os.arch
- bitness = System.getProperty("os.arch");
- if(bitness != null) {
- bitness = bitness.toLowerCase(Locale.ENGLISH);
- // 32-bit is now much less common, code to the exceptions
- if(bitness.equals("arm") || bitness.endsWith("86") || bitness.endsWith("32")) {
- // i386, x86
+ // fallback on some sane, hard-coded values
+ switch(SystemUtilities.getJreArch()) {
+ case ARM:
+ case X86:
return "32";
- }
+ case X86_64:
+ case RISCV:
+ case PPC:
+ default:
+ return "64";
}
- return "64";
}
private static String getPlatform(boolean legacy) {
@@ -199,9 +198,9 @@ private static String getPlatform(boolean legacy) {
if (!GraphicsEnvironment.isHeadless() && parts != null && parts.length > 2) {
linuxOS = parts[2];
}
- if (SystemUtilities.isUbuntu()) {
+ if (UnixUtilities.isUbuntu()) {
linuxOS += (linuxOS.isEmpty() ? "" : "; ") + "Ubuntu";
- } else if(SystemUtilities.isFedora()) {
+ } else if(UnixUtilities.isFedora()) {
linuxOS += (linuxOS.isEmpty()? "" : "; ") + "Fedora";
}
return linuxOS;
@@ -236,13 +235,27 @@ private static String getUserAgentOS() {
}
private static String getUserAgentArch() {
- String arch = System.getProperty("os.arch");
- arch = "amd64".equalsIgnoreCase(arch) ? "x86_64" : arch;
- if (SystemUtilities.isWow64()) {
- return "WOW64";
- } else if(SystemUtilities.isLinux()) {
- return "Linux " + arch;
+ String arch;
+ switch (SystemUtilities.getJreArch()) {
+ case X86:
+ arch = "x86";
+ break;
+ case X86_64:
+ arch = "x86_64";
+ break;
+ default:
+ arch = SystemUtilities.OS_ARCH;
+ }
+
+ switch(SystemUtilities.getOsType()) {
+ case LINUX:
+ return "Linux " + arch;
+ case WINDOWS:
+ if(WindowsUtilities.isWow64()) {
+ return "WOW64";
+ }
+ default:
+ return arch;
}
- return arch;
}
}
diff --git a/src/qz/utils/FileUtilities.java b/src/qz/utils/FileUtilities.java
index 8483ac47c..4634aba0b 100644
--- a/src/qz/utils/FileUtilities.java
+++ b/src/qz/utils/FileUtilities.java
@@ -21,6 +21,7 @@
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
+import qz.App;
import qz.auth.Certificate;
import qz.auth.RequestState;
import qz.common.ByteArrayBuilder;
@@ -28,10 +29,9 @@
import qz.common.PropertyHelper;
import qz.communication.FileIO;
import qz.communication.FileParams;
-import qz.installer.WindowsSpecialFolders;
import qz.exception.NullCommandException;
+import qz.installer.WindowsSpecialFolders;
import qz.installer.certificate.CertificateManager;
-import qz.ws.PrintSocketServer;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -106,7 +106,11 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
*/
private static Path getUserDirectory() {
if(SystemUtilities.isWindows()) {
- return Paths.get(WindowsSpecialFolders.ROAMING_APPDATA.getPath(), Constants.DATA_DIR);
+ try {
+ return Paths.get(WindowsSpecialFolders.ROAMING_APPDATA.getPath(), Constants.DATA_DIR);
+ } catch(Throwable ignore) {
+ return Paths.get(System.getenv("APPDATA"), Constants.DATA_DIR);
+ }
} else if(SystemUtilities.isMac()) {
return Paths.get(System.getProperty("user.home"), "/Library/Application Support/", Constants.DATA_DIR);
} else {
@@ -119,7 +123,11 @@ private static Path getUserDirectory() {
*/
private static Path getSharedDirectory() {
if(SystemUtilities.isWindows()) {
- return Paths.get(WindowsSpecialFolders.PROGRAM_DATA.getPath(), Constants.DATA_DIR);
+ try {
+ return Paths.get(WindowsSpecialFolders.PROGRAM_DATA.getPath(), Constants.DATA_DIR);
+ } catch(Throwable ignore) {
+ return Paths.get(System.getenv("PROGRAMDATA"), Constants.DATA_DIR);
+ }
} else if(SystemUtilities.isMac()) {
return Paths.get("/Library/Application Support/", Constants.DATA_DIR);
} else {
@@ -128,12 +136,9 @@ private static Path getSharedDirectory() {
}
public static boolean childOf(File childFile, Path parentPath) {
- Path child = childFile.toPath().toAbsolutePath();
- Path parent = parentPath.toAbsolutePath();
- if(SystemUtilities.isWindows()) {
- return child.toString().toLowerCase(Locale.ENGLISH).startsWith(parent.toString().toLowerCase(Locale.ENGLISH));
- }
- return child.toString().startsWith(parent.toString());
+ Path child = childFile.toPath().normalize().toAbsolutePath();
+ Path parent = parentPath.normalize().toAbsolutePath();
+ return child.startsWith(parent);
}
public static Path inheritParentPermissions(Path filePath) {
@@ -297,7 +302,7 @@ public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean san
//default sandbox locations. More can be added through the properties file
whiteList.add(new AbstractMap.SimpleEntry<>(USER_DIR, FIELD_SEPARATOR + "sandbox" + FIELD_SEPARATOR));
whiteList.add(new AbstractMap.SimpleEntry<>(SHARED_DIR, FIELD_SEPARATOR + "sandbox" + FIELD_SEPARATOR));
- whiteList.addAll(parseDelimitedPaths(getFileAllowProperty(PrintSocketServer.getTrayProperties()).toString()));
+ whiteList.addAll(parseDelimitedPaths(getFileAllowProperty(App.getTrayProperties()).toString()));
}
Path cleanPath = path.normalize().toAbsolutePath();
@@ -321,12 +326,6 @@ public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean san
return false;
}
- public static String getParentDirectory(String filePath) {
- // Working path should always default to the JARs parent folder
- int lastSlash = filePath.lastIndexOf(File.separator);
- return lastSlash < 0? "":filePath.substring(0, lastSlash);
- }
-
public static ArgParser.ExitStatus addFileAllowProperty(String path, String commonName) throws IOException {
PropertyHelper props = new PropertyHelper(new File(CertificateManager.getWritableLocation(), Constants.PROPS_FILE + ".properties"));
ArrayList> paths = parseDelimitedPaths(getFileAllowProperty(props).toString(), false);
@@ -788,6 +787,9 @@ public static void configureAssetFile(String relativeAsset, File dest, HashMap additionalMappings, Class relativeClass) throws IOException {
+ configureAssetFile(relativeAsset, dest.toFile(), additionalMappings, relativeClass);
+ }
public static Path getTempDirectory() {
try {
diff --git a/src/qz/utils/GtkUtilities.java b/src/qz/utils/GtkUtilities.java
index 191ec4be2..1aa04539d 100644
--- a/src/qz/utils/GtkUtilities.java
+++ b/src/qz/utils/GtkUtilities.java
@@ -15,17 +15,21 @@ public class GtkUtilities {
* Initializes Gtk2/3 and returns the desktop scaling factor, usually 1.0 or 2.0
*/
public static double getScaleFactor() {
- GTK gtkHandle = getGtkInstance();
- if (gtkHandle != null && gtkHandle.gtk_init_check(0, null)) {
- log.debug("Initialized Gtk");
-
- if (gtkHandle instanceof GTK2) {
- return getGtk2ScaleFactor((GTK2)gtkHandle);
+ try {
+ GTK gtkHandle = getGtkInstance();
+ if (gtkHandle != null && gtkHandle.gtk_init_check(0, null)) {
+ log.debug("Initialized Gtk");
+
+ if (gtkHandle instanceof GTK2) {
+ return getGtk2ScaleFactor((GTK2)gtkHandle);
+ } else {
+ return getGtk3ScaleFactor((GTK3)gtkHandle);
+ }
} else {
- return getGtk3ScaleFactor((GTK3)gtkHandle);
+ log.warn("An error occurred initializing the Gtk library");
}
- } else {
- log.warn("An error occurred initializing the Gtk library");
+ } catch(Throwable t) {
+ log.warn("An error occurred initializing the Gtk library", t);
}
return 0;
}
diff --git a/src/qz/utils/JsonWriter.java b/src/qz/utils/JsonWriter.java
index eecb3ad45..93ed03e02 100644
--- a/src/qz/utils/JsonWriter.java
+++ b/src/qz/utils/JsonWriter.java
@@ -21,7 +21,6 @@
import java.io.File;
import java.io.IOException;
-import java.util.Collections;
import java.util.Iterator;
/**
diff --git a/src/qz/utils/LibUtilities.java b/src/qz/utils/LibUtilities.java
new file mode 100644
index 000000000..a6790dc5c
--- /dev/null
+++ b/src/qz/utils/LibUtilities.java
@@ -0,0 +1,193 @@
+package qz.utils;
+
+import com.sun.jna.Platform;
+import javafx.application.Application;
+import org.usb4java.Loader;
+import qz.common.Constants;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Helper for setting various booth paths for finding native libraries
+ * e.g. "java.library.path", "jna.boot.library.path" ... etc
+ */
+public class LibUtilities {
+ // Files indicating whether or not we can load natives from an external location
+ private static final String[] INDICATOR_RESOURCES = {"/com/sun/jna/" + Platform.RESOURCE_PREFIX };
+ private static final String LIB_DIR = "./libs";
+ private static final String MAC_LIB_DIR = "../Frameworks";
+
+ private static final LibUtilities INSTANCE = new LibUtilities();
+
+ // Track if libraries are externalized into a "libs" folder
+ private final boolean externalized;
+
+ // The base library path
+ private final Path basePath;
+
+ // Common native file extensions, by platform
+ private static HashMap extensionMap = new HashMap<>();
+ static {
+ extensionMap.put(SystemUtilities.OsType.WINDOWS, new String[]{ "dll" });
+ extensionMap.put(SystemUtilities.OsType.MAC, new String[]{ "jnilib", "dylib" });
+ extensionMap.put(SystemUtilities.OsType.LINUX, new String[]{ "so" });
+ extensionMap.put(SystemUtilities.OsType.UNKNOWN, new String[]{ "so" });
+ }
+
+ public LibUtilities() {
+ this(calculateBasePath(), calculateExternalized());
+ }
+
+ public LibUtilities(Path basePath, boolean externalized) {
+ this.basePath = basePath;
+ this.externalized = externalized;
+ }
+
+ public static LibUtilities getInstance() {
+ return INSTANCE;
+ }
+
+ public void bind() {
+ if (externalized) {
+ bindProperties("jna.boot.library.path", // jna
+ "jna.library.path", // hid4java
+ "jssc.boot.library.path" // jssc
+ );
+ bindUsb4Java();
+ }
+ // JavaFX is always externalized
+ if (Constants.JAVA_VERSION.getMajorVersion() >= 11) {
+ // Calculate basePath for IDE
+ Path fxBase = SystemUtilities.isJar()? basePath:
+ findNativeLib("glass", SystemUtilities.getJarParentPath("../lib").normalize());
+ bindProperty("java.library.path", fxBase); // javafx
+ }
+ }
+
+ /**
+ * Search recursively for a native library in the specified path
+ */
+ private static Path findNativeLib(String libName, Path basePath) {
+ String[] extensions = extensionMap.get(SystemUtilities.getOsType());
+ String prefix = !SystemUtilities.isWindows() ? "lib" : "";
+ List found = new ArrayList<>();
+ try (Stream walkStream = Files.walk(basePath)) {
+ walkStream.filter(p -> p.toFile().isFile()).forEach(f -> {
+ for(String extension : extensions) {
+ if (f.getFileName().toString().equals(prefix + libName + "." + extension)) {
+ found.add(f.getParent());
+ }
+ }
+ });
+ } catch(IOException ignore) {}
+ return found.size() > 0 ? found.get(0) : null;
+ }
+
+ /**
+ * Calculates the base native library path based on the jar path
+ */
+ private static Path calculateBasePath() {
+ return SystemUtilities.getJarParentPath().resolve(
+ useFrameworks() ? MAC_LIB_DIR : LIB_DIR
+ ).normalize();
+ }
+
+ /**
+ * Whether to use the standard "libs" directory
+ */
+ private static boolean useFrameworks() {
+ return SystemUtilities.isMac() && SystemUtilities.isInstalled();
+ }
+
+ private void bindProperties(String ... properties) {
+ Arrays.stream(properties).forEach(this::bindProperty);
+ }
+
+ /**
+ * Binds a system property to the calculated basePath
+ */
+ private void bindProperty(String property) {
+ bindProperty(property, basePath);
+ }
+
+ /**
+ * Binds a system property to the specified basePath
+ */
+ private void bindProperty(String property, Path basePath) {
+ if(property == null || basePath == null) {
+ return;
+ }
+ if(!property.equals("java.library.path")) {
+ System.setProperty(property, basePath.toString());
+ } else {
+ // Special case for "java.library.path", used by JavaFX
+ SystemUtilities.insertPathProperty(
+ "java.library.path",
+ basePath.toString(),
+ "/jni" /* appends to end if not found */
+ );
+ }
+ }
+
+ /**
+ * Using reflection, force usb4java to load from the specified boot path
+ */
+ private void bindUsb4Java() {
+ try {
+ // Make usb4java think it's already unzipped it's native resources
+ Field loaded = Loader.class.getDeclaredField("loaded");
+ loaded.setAccessible(true);
+ loaded.set(Loader.class, true);
+
+ // Expose private functions (getExtraLibName is only needed for Windows)
+ Method getPlatform = Loader.class.getDeclaredMethod("getPlatform");
+ Method getLibName = Loader.class.getDeclaredMethod("getLibName");
+ Method getExtraLibName = Loader.class.getDeclaredMethod("getExtraLibName");
+ getPlatform.setAccessible(true);
+ getLibName.setAccessible(true);
+ getExtraLibName.setAccessible(true);
+
+ // Simulate Loader.load's path calculation
+ String lib = (String)getLibName.invoke(Loader.class);
+ String extraLib = (String)getExtraLibName.invoke(Loader.class);
+ if (extraLib != null) System.load(basePath.resolve(extraLib).toString());
+ System.load(basePath.resolve(lib).toString());
+ } catch(Throwable ignore) {}
+ }
+
+ // TODO: Determine fx "libs" or "${basedir}/lib/javafx" for the running jre and remove
+ private boolean detectJavaFxConflict() {
+ // If running from the IDE, make sure we're not using the wrong libs
+ URL url = Application.class.getResource("/" + Application.class.getName().replace('.', '/') + ".class");
+ String graphicsJar = url.toString().replaceAll("file:/|jar:", "").replaceAll("!.*", "");
+ switch(SystemUtilities.getOsType()) {
+ case WINDOWS:
+ return !graphicsJar.contains("windows");
+ case MAC:
+ return !graphicsJar.contains("osx") && !graphicsJar.contains("mac");
+ default:
+ return !graphicsJar.contains("linux");
+ }
+ }
+
+ /**
+ * Detect if the JAR has native resources bundled, if not, we'll assume they've been externalized
+ */
+ private static boolean calculateExternalized() {
+ for(String resource : INDICATOR_RESOURCES)
+ if(SystemUtilities.class.getResource(resource) != null)
+ return false;
+
+ return true;
+ }
+}
diff --git a/src/qz/utils/MacUtilities.java b/src/qz/utils/MacUtilities.java
index e506bb890..aad51afbc 100644
--- a/src/qz/utils/MacUtilities.java
+++ b/src/qz/utils/MacUtilities.java
@@ -11,25 +11,18 @@
package qz.utils;
import com.apple.OSXAdapterWrapper;
+import org.dyorgio.jna.platform.mac.*;
import com.github.zafarkhaja.semver.Version;
-import com.sun.jna.Library;
-import com.sun.jna.Native;
import com.sun.jna.NativeLong;
-import com.sun.jna.Pointer;
-import org.dyorgio.jna.platform.mac.ActionCallback;
-import org.dyorgio.jna.platform.mac.Foundation;
-import org.dyorgio.jna.platform.mac.FoundationUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
import qz.common.TrayManager;
-import qz.ui.component.IconCache;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
-import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
@@ -44,14 +37,13 @@
* @author Tres Finocchiaro
*/
public class MacUtilities {
-
- private static final Logger log = LoggerFactory.getLogger(IconCache.class);
+ private static final Logger log = LoggerFactory.getLogger(MacUtilities.class);
private static Dialog aboutDialog;
private static TrayManager trayManager;
private static String bundleId;
- private static Integer pid;
private static Boolean jdkSupportsTemplateIcon;
private static boolean templateIconForced = false;
+ private static boolean sandboxed = System.getenv("APP_SANDBOX_CONTAINER_ID") != null;
public static void showAboutDialog() {
if (aboutDialog != null) { aboutDialog.setVisible(true); }
@@ -124,7 +116,12 @@ public static void registerQuitHandler(TrayManager trayManager) {
* @return true if enabled, false if not
*/
public static boolean isDarkDesktop() {
- return !ShellUtilities.execute(new String[] { "defaults", "read", "-g", "AppleInterfaceStyle" }, new String[] { "Dark" }, true, true).isEmpty();
+ try {
+ return "Dark".equalsIgnoreCase(NSUserDefaults.standard().stringForKey(new NSString("AppleInterfaceStyle")).toString());
+ } catch(Exception e) {
+ log.warn("An exception occurred obtaining theme information, falling back to command line instead.");
+ return !ShellUtilities.execute(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"}, new String[] {"Dark"}, true, true).isEmpty();
+ }
}
public static int getScaleFactor() {
@@ -149,24 +146,6 @@ public static int getScaleFactor() {
return 1;
}
- static int getProcessId() {
- if(pid == null) {
- try {
- pid = CLibrary.INSTANCE.getpid();
- }
- catch(UnsatisfiedLinkError | NoClassDefFoundError e) {
- log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1.");
- pid = -1;
- }
- }
- return pid;
- }
-
- private interface CLibrary extends Library {
- CLibrary INSTANCE = Native.load("c", CLibrary.class);
- int getpid ();
- }
-
/**
* Checks for presence of JDK-8252015 using reflection
*/
@@ -253,6 +232,17 @@ public static void toggleTemplateIcon(TrayIcon icon) {
} catch (Throwable ignore) {}
}
+ public static void setFocus() {
+ try {
+ NSApplication.sharedApplication().activateIgnoringOtherApps(true);
+ } catch(Throwable t) {
+ log.warn("Couldn't set focus using JNA, falling back to command line instead");
+ ShellUtilities.executeAppleScript("tell application \"System Events\" \n" +
+ "set frontmost of every process whose unix id is " + UnixUtilities.getProcessId() + " to true \n" +
+ "end tell");
+ }
+ }
+
public static boolean nativeFileCopy(Path source, Path destination) {
try {
// AppleScript's "duplicate" requires an existing destination
@@ -275,4 +265,7 @@ public static boolean nativeFileCopy(Path source, Path destination) {
return false;
}
+ public static boolean isSandboxed() {
+ return sandboxed;
+ }
}
diff --git a/src/qz/utils/NetworkUtilities.java b/src/qz/utils/NetworkUtilities.java
index c011bd105..bde69aa0a 100644
--- a/src/qz/utils/NetworkUtilities.java
+++ b/src/qz/utils/NetworkUtilities.java
@@ -9,6 +9,7 @@
*/
package qz.utils;
+import com.sun.jna.platform.win32.Kernel32Util;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
@@ -29,7 +30,7 @@ public class NetworkUtilities {
private static final Logger log = LoggerFactory.getLogger(NetworkUtilities.class);
private static NetworkUtilities instance;
- private static String systemName = ShellUtilities.getHostName();
+ private static String systemName = SystemUtilities.getHostName();
private static String userName = System.getProperty("user.name");
// overridable in preferences, see "networking.hostname", "networking.port"
diff --git a/src/qz/utils/ShellUtilities.java b/src/qz/utils/ShellUtilities.java
index e49590ac1..f0e0a1a63 100644
--- a/src/qz/utils/ShellUtilities.java
+++ b/src/qz/utils/ShellUtilities.java
@@ -16,6 +16,7 @@
import java.awt.*;
import java.io.*;
+import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
@@ -212,8 +213,11 @@ public static String executeRaw(String[] commandArray, boolean silent) {
/**
* Gets the computer's "hostname" from command line
+ *
+ * This should only be used as a fallback for when JNA is not available,
+ * see SystemUtilities.getHostName()
instead.
*/
- public static String getHostName() {
+ static String getHostName() {
return execute(new String[] {"hostname"}, new String[] {""});
}
@@ -235,7 +239,7 @@ public static boolean executeAppleScript(String scriptBody) {
}
public static void browseAppDirectory() {
- browseDirectory(FileUtilities.getParentDirectory(SystemUtilities.getJarPath()));
+ browseDirectory(SystemUtilities.getJarParentPath());
}
public static void browseDirectory(String directory) {
@@ -251,10 +255,18 @@ public static void browseDirectory(File directory) {
if (!SystemUtilities.isMac()) {
Desktop.getDesktop().open(directory);
} else {
- // Mac tries to open the .app rather than browsing it. Instead, pass a child with -R to select it in finder
+ // Mac tries to open the .app rather than browsing it. Instead, pass a child to select it in finder
File[] files = directory.listFiles();
if (files != null && files.length > 0) {
- ShellUtilities.execute("open", "-R", files[0].getCanonicalPath());
+ try {
+ // Use browseFileDirectory (JDK9+) via reflection
+ Method m = Desktop.class.getDeclaredMethod("browseFileDirectory", File.class);
+ m.invoke(Desktop.getDesktop(), files[0].getCanonicalFile());
+ }
+ catch(ReflectiveOperationException e) {
+ // Fallback to open -R
+ ShellUtilities.execute("open", "-R", files[0].getCanonicalPath());
+ }
}
}
}
diff --git a/src/qz/utils/SystemUtilities.java b/src/qz/utils/SystemUtilities.java
index a85e1037e..675e56286 100644
--- a/src/qz/utils/SystemUtilities.java
+++ b/src/qz/utils/SystemUtilities.java
@@ -23,10 +23,12 @@
import javax.swing.*;
import java.awt.*;
import java.io.File;
-import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
@@ -44,9 +46,10 @@
* @author Tres Finocchiaro
*/
public class SystemUtilities {
-
- // Name of the os, i.e. "Windows XP", "Mac OS X"
- private static final String OS_NAME = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
+ static final String OS_NAME = System.getProperty("os.name");
+ static final String OS_ARCH = System.getProperty("os.arch");
+ private static final OsType OS_TYPE = getOsType(OS_NAME);
+ private static final JreArch JRE_ARCH = getJreArch(OS_ARCH);
private static final Logger log = LoggerFactory.getLogger(TrayManager.class);
private static final Locale defaultLocale = Locale.getDefault();
@@ -62,19 +65,76 @@ public class SystemUtilities {
private static Boolean darkDesktop;
private static Boolean darkTaskbar;
private static Boolean hasMonocle;
- private static String uname;
- private static String linuxRelease;
private static String classProtocol;
private static Version osVersion;
- private static String jarPath;
+ private static Path jarPath;
+ private static Integer pid;
+
+ public enum OsType {
+ MAC,
+ WINDOWS,
+ LINUX,
+ SOLARIS,
+ UNKNOWN
+ }
+
+ public enum JreArch {
+ X86,
+ X86_64,
+ ARM, // 32-bit
+ AARCH64,
+ RISCV,
+ PPC,
+ UNKNOWN
+ }
+
+ public static OsType getOsType() {
+ return OS_TYPE;
+ }
+
+ public static OsType getOsType(String os) {
+ if(os != null) {
+ String osLower = os.toLowerCase(Locale.ENGLISH);
+ if (osLower.contains("win")) {
+ return OsType.WINDOWS;
+ } else if (osLower.contains("mac")) {
+ return OsType.MAC;
+ } else if (osLower.contains("linux")) {
+ return OsType.LINUX;
+ } else if (osLower.contains("sunos")) {
+ return OsType.SOLARIS;
+ }
+ }
+ return OsType.UNKNOWN;
+ }
+ public static JreArch getJreArch() {
+ return JRE_ARCH;
+ }
- /**
- * @return Lowercase version of the operating system name
- * identified by {@code System.getProperty("os.name");}.
- */
- public static String getOS() {
- return OS_NAME;
+ public static JreArch getJreArch(String arch) {
+ if(arch != null) {
+ String archLower = arch.toLowerCase(Locale.ENGLISH);
+ if (archLower.equals("arm")) {
+ return JreArch.ARM;
+ }
+ if (archLower.contains("amd64") || archLower.contains("x86_64")) {
+ return JreArch.X86_64;
+ }
+ if (archLower.contains("86")) { // x86, i386, i486, i586, i686
+ return JreArch.X86;
+ }
+ if (archLower.startsWith("aarch") || archLower.startsWith("arm")) {
+ return JreArch.AARCH64;
+ }
+ if (archLower.startsWith("riscv") || archLower.startsWith("rv")) {
+ return JreArch.RISCV;
+ }
+ if (archLower.startsWith("ppc") || archLower.startsWith("power")) {
+ return JreArch.PPC;
+ }
+ }
+ return JreArch.UNKNOWN;
}
/**
@@ -119,27 +179,71 @@ public static Version getOSVersion() {
}
public static boolean isAdmin() {
- if (SystemUtilities.isWindows()) {
- return ShellUtilities.execute("net", "session");
- } else {
- return ShellUtilities.executeRaw("whoami").trim().equals("root");
+ switch(OS_TYPE) {
+ case WINDOWS:
+ return ShellUtilities.execute("net", "session");
+ default:
+ return whoami().equals("root");
+ }
+ }
+
+ public static String whoami() {
+ String whoami = System.getProperty("user.name");
+ if(whoami == null || whoami.trim().isEmpty()) {
+ // Fallback on Command line
+ whoami = ShellUtilities.executeRaw("whoami").trim();
}
+ return whoami;
+ }
+
+ public static Version getJavaVersion() {
+ return getJavaVersion(System.getProperty("java.version"));
+ }
+
+ /**
+ * Call a java command (e.g. java) with "--version" and parse the output
+ * The double dash "--" is since JDK9 but important to send the command output to stdout
+ */
+ public static Version getJavaVersion(Path javaCommand) {
+ return getJavaVersion(ShellUtilities.executeRaw(javaCommand.toString(), "--version"));
}
public static int getProcessId() {
- if(isWindows()) {
- return WindowsUtilities.getProcessId();
+ if(pid == null) {
+ // Try Java 9+
+ if(Constants.JAVA_VERSION.getMajorVersion() >= 9) {
+ pid = getProcessIdJigsaw();
+ }
+ // Try JNA
+ if(pid == null || pid == -1) {
+ pid = SystemUtilities.isWindows() ? WindowsUtilities.getProcessId() : UnixUtilities.getProcessId();
+ }
}
- return MacUtilities.getProcessId(); // works for Linux too
+ return pid;
+ }
+
+ private static int getProcessIdJigsaw() {
+ try {
+ Class processHandle = Class.forName("java.lang.ProcessHandle");
+ Method current = processHandle.getDeclaredMethod("current");
+ Method pid = processHandle.getDeclaredMethod("pid");
+ Object processHandleInstance = current.invoke(processHandle);
+ Object pidValue = pid.invoke(processHandleInstance);
+ if(pidValue instanceof Long) {
+ return ((Long)pidValue).intValue();
+ }
+ } catch(Throwable t) {
+ log.warn("Could not get process ID using Java 9+, will attempt to fallback to JNA", t);
+ }
+ return -1;
}
/**
* Handle Java versioning nuances
* To eventually be replaced with java.lang.Runtime.Version
(JDK9+)
*/
- public static Version getJavaVersion() {
- String version = System.getProperty("java.version");
- String[] parts = version.split("\\D+");
+ public static Version getJavaVersion(String version) {
+ String[] parts = version.trim().split("\\D+");
int major = 1;
int minor = 0;
@@ -177,56 +281,59 @@ public static Version getJavaVersion() {
/**
* Determines the currently running Jar's absolute path on the local filesystem
+ * todo: make this return a sane directory for running via ide
*
* @return A String value representing the absolute path to the currently running
* jar
*/
- public static String detectJarPath() {
+ public static Path getJarPath() {
+ // jarPath won't change, send the cached value if we have it
+ if (jarPath != null) return jarPath;
try {
- String jarPath = new File(SystemUtilities.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getCanonicalPath();
- // Fix characters that get URL encoded when calling getPath()
- return URLDecoder.decode(jarPath, "UTF-8");
- } catch(IOException ex) {
+ String url = URLDecoder.decode(SystemUtilities.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8");
+ jarPath = new File(url).toPath();
+ if (jarPath == null) return null;
+ jarPath = jarPath.toAbsolutePath();
+ } catch(InvalidPathException | UnsupportedEncodingException ex) {
log.error("Unable to determine Jar path", ex);
}
- return null;
+ return jarPath;
}
/**
- * Returns the jar which we will create a shortcut for
- *
- * @return The path to the jar path which has been set
+ * Returns the folder containing the running jar
+ * or null if no .jar is found (such as running from IDE)
*/
- public static String getJarPath() {
- if (jarPath == null) {
- jarPath = detectJarPath();
- }
- return jarPath;
+ public static Path getJarParentPath(){
+ Path path = getJarPath();
+ if (path == null || path.getParent() == null) return null;
+ return path.getParent();
}
/**
- * Returns the app's path, based on the jar location
- * or null if no .jar is found (such as running from IDE)
- * @return
+ * Returns the jar's parent path, or a fallback if we're not a jar
*/
- public static Path detectAppPath() {
- String jarPath = detectJarPath();
- if (jarPath != null) {
- File jar = new File(jarPath);
- if (jar.getPath().endsWith(".jar") && jar.exists()) {
- return Paths.get(jar.getParent());
- }
- }
- return null;
+ public static Path getJarParentPath(String relativeFallback) {
+ return getJarParentPath().resolve(SystemUtilities.isJar() ? "": relativeFallback).normalize();
}
/**
- * Detect 32-bit JVM on 64-bit Windows
- * @return
+ * Returns the app's path, calculated from the jar location
+ * or working directory if none can be found
*/
- public static boolean isWow64() {
- String arch = System.getProperty("os.arch");
- return isWindows() && !arch.contains("x86_64") && !arch.contains("amd64") && System.getenv("PROGRAMFILES(x86)") != null;
+ public static Path getAppPath() {
+ Path appPath = getJarParentPath();
+ if(appPath == null) {
+ // We should never get here
+ appPath = Paths.get(System.getProperty("user.dir"));
+ }
+
+ // Assume we're installed and running from /Applications/QZ Tray.app/Contents/Resources/qz-tray.jar
+ if(appPath.endsWith("Resources")) {
+ return appPath.getParent().getParent();
+ }
+ // For all other use-cases, qz-tray.jar is installed in the root of the application
+ return appPath;
}
/**
@@ -235,18 +342,16 @@ public static boolean isWow64() {
* @return {@code true} if Windows, {@code false} otherwise
*/
public static boolean isWindows() {
- return (OS_NAME.contains("win"));
+ return OS_TYPE == OsType.WINDOWS;
}
- public static boolean isWindowsXP() { return OS_NAME.contains("win") && OS_NAME.contains("xp"); }
-
/**
* Determine if the current Operating System is Mac OS
*
* @return {@code true} if Mac OS, {@code false} otherwise
*/
public static boolean isMac() {
- return (OS_NAME.contains("mac"));
+ return OS_TYPE == OsType.MAC;
}
/**
@@ -255,7 +360,7 @@ public static boolean isMac() {
* @return {@code true} if Linux, {@code false} otherwise
*/
public static boolean isLinux() {
- return (OS_NAME.contains("linux"));
+ return OS_TYPE == OsType.LINUX;
}
/**
@@ -264,7 +369,12 @@ public static boolean isLinux() {
* @return {@code true} if Unix, {@code false} otherwise
*/
public static boolean isUnix() {
- return (OS_NAME.contains("mac") || OS_NAME.contains("nix") || OS_NAME.contains("nux") || OS_NAME.indexOf("aix") > 0 || OS_NAME.contains("sunos"));
+ if(OS_NAME != null) {
+ String osLower = OS_NAME.toLowerCase(Locale.ENGLISH);
+ return OS_TYPE == OsType.MAC || OS_TYPE == OsType.SOLARIS || OS_TYPE == OsType.LINUX ||
+ osLower.contains("nix") || osLower.indexOf("aix") > 0;
+ }
+ return false;
}
/**
@@ -273,66 +383,7 @@ public static boolean isUnix() {
* @return {@code true} if Solaris, {@code false} otherwise
*/
public static boolean isSolaris() {
- return (OS_NAME.contains("sunos"));
- }
-
- /**
- * Returns whether the output of {@code uname -a} shell command contains "Ubuntu"
- *
- * @return {@code true} if this OS is Ubuntu
- */
- public static boolean isUbuntu() {
- getUname();
- return uname != null && uname.contains("Ubuntu");
- }
-
- /**
- * Returns whether the output of cat /etc/redhat-release/code> shell command contains "Fedora"
- *
- * @return {@code true} if this OS is Fedora
- */
- public static boolean isFedora() {
- getLinuxRelease();
- return linuxRelease != null && linuxRelease.contains("Fedora");
- }
-
- /**
- * Returns the output of {@code cat /etc/lsb-release} or equivalent
- *
- * @return the output of the command or null if not running Linux
- */
- public static String getLinuxRelease() {
- if (isLinux() && linuxRelease == null) {
- String[] releases = {"/etc/lsb-release", "/etc/redhat-release"};
- for(String release : releases) {
- String result = ShellUtilities.execute(
- new String[] {"cat", release},
- null
- );
- if (!result.isEmpty()) {
- linuxRelease = result;
- break;
- }
- }
- }
-
- return linuxRelease;
- }
-
- /**
- * Returns the output of {@code uname -a} shell command, useful for parsing the Linux Version
- *
- * @return the output of {@code uname -a}, or null if not running Linux
- */
- public static String getUname() {
- if (isLinux() && uname == null) {
- uname = ShellUtilities.execute(
- new String[] {"uname", "-a"},
- null
- );
- }
-
- return uname;
+ return OS_TYPE == OsType.SOLARIS;
}
public static boolean isDarkTaskbar() {
@@ -381,7 +432,7 @@ public static boolean prefersMaskTrayIcon() {
if (Constants.MASK_TRAY_SUPPORTED) {
if (SystemUtilities.isMac()) {
// Assume a pid of -1 is a broken JNA
- return MacUtilities.getProcessId() != -1;
+ return getProcessId() != -1;
} else if (SystemUtilities.isWindows() && SystemUtilities.getOSVersion().getMajorVersion() >= 10) {
return true;
}
@@ -491,6 +542,19 @@ public static boolean isJar() {
return "jar".equals(classProtocol);
}
+ /**
+ * Todo:
+ * @return true if running from a jar, false if running from IDE
+ */
+ public static boolean isInstalled() {
+ Path path = getJarParentPath();
+ if(path == null) {
+ return false;
+ }
+ // Assume dist or out are signs we're running from some form of build directory
+ return !path.endsWith("dist") && !path.endsWith("out");
+ }
+
/**
* Allows in-line insertion of a property before another
* @param value the end of a value to insert before, assumes to end with File.pathSeparator
@@ -607,6 +671,15 @@ public static void clearAlgorithms() {
}
}
+ public static String getHostName() {
+ String hostName = SystemUtilities.isWindows() ? WindowsUtilities.getHostName() : UnixUtilities.getHostName();
+ if(hostName == null || hostName.trim().isEmpty()) {
+ log.warn("Couldn't get hostname using internal techniques, will fallback to command line instead");
+ hostName = ShellUtilities.getHostName().toUpperCase(); // uppercase to match others
+ }
+ return hostName;
+ }
+
/**
* A challenge which can only be calculated by an app installed and running on this machine
* Calculates two bytes:
@@ -626,9 +699,8 @@ public static String calculateSaltedChallenge() {
private static long calculateChallenge() {
if(getJarPath() != null) {
- File jarFile = new File(getJarPath());
- if (jarFile.exists()) {
- return jarFile.lastModified();
+ if (getJarPath().toFile().exists()) {
+ return getJarPath().toFile().lastModified();
}
}
return -1L; // Fallback when running from IDE
diff --git a/src/qz/utils/UbuntuUtilities.java b/src/qz/utils/UbuntuUtilities.java
index 68ca2e523..97920296d 100644
--- a/src/qz/utils/UbuntuUtilities.java
+++ b/src/qz/utils/UbuntuUtilities.java
@@ -15,14 +15,6 @@
import qz.ui.component.IconCache;
import java.awt.*;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Pattern;
/**
* Utility class for Ubuntu OS specific functions.
@@ -107,7 +99,7 @@ public static String getThemeName(String defaultTheme) {
*/
public static void fixTrayIcons(IconCache iconCache) {
// Execute some shell commands to determine specific Linux OS
- if (SystemUtilities.isUbuntu()) {
+ if (UnixUtilities.isUbuntu()) {
for(IconCache.Icon i : IconCache.getTypes()) {
if (i.isTrayIcon()) {
iconCache.setBgColor(i, getTrayColor());
diff --git a/src/qz/utils/UnixUtilities.java b/src/qz/utils/UnixUtilities.java
new file mode 100644
index 000000000..e5a8f3f93
--- /dev/null
+++ b/src/qz/utils/UnixUtilities.java
@@ -0,0 +1,121 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+package qz.utils;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.platform.unix.LibC;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helper functions for both Linux and MacOS
+ */
+public class UnixUtilities {
+ private static final Logger log = LoggerFactory.getLogger(UnixUtilities.class);
+ private static String uname;
+ private static String linuxRelease;
+ private static Integer pid;
+
+ static String getHostName() {
+ String hostName = null;
+ try {
+ byte[] bytes = new byte[255];
+ if (LibC.INSTANCE.gethostname(bytes, bytes.length) == 0) {
+ hostName = Native.toString(bytes);
+ }
+ } catch(Throwable ignore) {}
+ return hostName;
+ }
+
+ static int getProcessId() {
+ if(pid == null) {
+ try {
+ pid = UnixUtilities.CLibrary.INSTANCE.getpid();
+ }
+ catch(UnsatisfiedLinkError | NoClassDefFoundError e) {
+ log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1.");
+ pid = -1;
+ }
+ }
+ return pid;
+ }
+
+ private interface CLibrary extends Library {
+ CLibrary INSTANCE = Native.load("c", CLibrary.class);
+ int getpid();
+ }
+
+ /**
+ * Returns the output of {@code uname -a} shell command, useful for parsing the Linux Version
+ *
+ * @return the output of {@code uname -a}, or null if not running Linux
+ */
+ public static String getUname() {
+ if (SystemUtilities.isUnix() && uname == null) {
+ uname = ShellUtilities.execute(
+ new String[] {"uname", "-a"},
+ null
+ );
+ }
+
+ return uname;
+ }
+
+ /**
+ * Returns the output of {@code cat /etc/lsb-release} or equivalent
+ *
+ * @return the output of the command or null if not running Linux
+ */
+ public static String getLinuxRelease() {
+ if (SystemUtilities.isLinux() && linuxRelease == null) {
+ String[] releases = {"/etc/lsb-release", "/etc/redhat-release"};
+ for(String release : releases) {
+ String result = ShellUtilities.execute(
+ new String[] {"cat", release},
+ null
+ );
+ if (!result.isEmpty()) {
+ linuxRelease = result;
+ break;
+ }
+ }
+ }
+
+ return linuxRelease;
+ }
+
+ /**
+ * Returns whether the output of {@code uname -a} shell command contains "Ubuntu"
+ *
+ * @return {@code true} if this OS is Ubuntu
+ */
+ public static boolean isUbuntu() {
+ if(!SystemUtilities.isLinux()) {
+ return false;
+ }
+ getUname();
+ return uname != null && uname.contains("Ubuntu");
+ }
+
+
+ /**
+ * Returns whether the output of cat /etc/redhat-release/code> shell command contains "Fedora"
+ *
+ * @return {@code true} if this OS is Fedora
+ */
+ public static boolean isFedora() {
+ if(!SystemUtilities.isLinux()) {
+ return false;
+ }
+ getLinuxRelease();
+ return linuxRelease != null && linuxRelease.contains("Fedora");
+ }
+}
diff --git a/src/qz/utils/WindowsUtilities.java b/src/qz/utils/WindowsUtilities.java
index 6d6992e6b..24425c372 100644
--- a/src/qz/utils/WindowsUtilities.java
+++ b/src/qz/utils/WindowsUtilities.java
@@ -1,9 +1,18 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
package qz.utils;
import com.github.zafarkhaja.semver.Version;
import com.sun.jna.Native;
import com.sun.jna.platform.win32.*;
-import com.sun.jna.win32.W32APIOptions;
+import com.sun.jna.ptr.IntByReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
@@ -15,9 +24,11 @@
import java.nio.file.Path;
import java.nio.file.attribute.*;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import static com.sun.jna.platform.win32.WinReg.*;
+import static qz.utils.SystemUtilities.*;
import static java.nio.file.attribute.AclEntryPermission.*;
import static java.nio.file.attribute.AclEntryFlag.*;
@@ -26,6 +37,8 @@ public class WindowsUtilities {
protected static final Logger log = LoggerFactory.getLogger(WindowsUtilities.class);
private static String THEME_REG_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
private static final String AUTHENTICATED_USERS_SID = "S-1-5-11";
+ private static Boolean isWow64;
+ private static Integer pid;
public static boolean isDarkDesktop() {
// 0 = Dark Theme. -1/1 = Light Theme
@@ -267,6 +280,22 @@ public static void setWritable(Path path) {
}
}
+ static String getHostName() {
+ String hostName = null;
+ try {
+ // GetComputerName() is limited to 15 chars, use GetComputerNameEx instead
+ char buffer[] = new char[255];
+ IntByReference lpnSize = new IntByReference(buffer.length);
+ Kernel32.INSTANCE.GetComputerNameEx(WinBase.COMPUTER_NAME_FORMAT.ComputerNameDnsHostname, buffer, lpnSize);
+ hostName = Native.toString(buffer).toUpperCase(Locale.ENGLISH); // Force uppercase for backwards compatibility
+ } catch(Throwable ignore) {}
+ if(hostName == null || hostName.trim().isEmpty()) {
+ log.warn("Couldn't get hostname using Kernel32, will fallback to environmental variable COMPUTERNAME instead");
+ hostName = System.getenv("COMPUTERNAME"); // always uppercase
+ }
+ return hostName;
+ }
+
public static boolean nativeFileCopy(Path source, Path destination) {
try {
ShellAPI.SHFILEOPSTRUCT op = new ShellAPI.SHFILEOPSTRUCT();
@@ -289,11 +318,36 @@ public static boolean elevatedFileCopy(Path source, Path destination) {
}
static int getProcessId() {
- try {
- return Kernel32.INSTANCE.GetCurrentProcessId();
- } catch(UnsatisfiedLinkError | NoClassDefFoundError e) {
- log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1.");
+ if(pid == null) {
+ try {
+ pid = Kernel32.INSTANCE.GetCurrentProcessId();
+ }
+ catch(UnsatisfiedLinkError | NoClassDefFoundError e) {
+ log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1.");
+ pid = -1;
+ }
+ }
+ return pid;
+ }
+
+ public static boolean isWindowsXP() {
+ return isWindows() && OS_NAME.contains("xp");
+ }
+
+
+ /**
+ * Detect 32-bit JVM on 64-bit Windows
+ * @return
+ */
+ public static boolean isWow64() {
+ if(isWow64 == null) {
+ isWow64 = false;
+ if (SystemUtilities.isWindows()) {
+ if (SystemUtilities.getJreArch() != JreArch.X86_64) {
+ isWow64 = System.getenv("PROGRAMFILES(x86)") != null;
+ }
+ }
}
- return -1;
+ return isWow64;
}
}
diff --git a/src/qz/ws/PrintSocketServer.java b/src/qz/ws/PrintSocketServer.java
index 90b35ece9..39867003b 100644
--- a/src/qz/ws/PrintSocketServer.java
+++ b/src/qz/ws/PrintSocketServer.java
@@ -10,11 +10,6 @@
package qz.ws;
-import org.apache.log4j.Level;
-import org.apache.log4j.PatternLayout;
-import org.apache.log4j.rolling.FixedWindowRollingPolicy;
-import org.apache.log4j.rolling.RollingFileAppender;
-import org.apache.log4j.rolling.SizeBasedTriggeringPolicy;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.server.*;
@@ -24,22 +19,17 @@
import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import qz.App;
import qz.common.Constants;
import qz.common.TrayManager;
-import qz.installer.Installer;
import qz.installer.certificate.CertificateManager;
-import qz.installer.certificate.ExpiryTask;
-import qz.installer.certificate.KeyPairWrapper;
-import qz.installer.certificate.NativeCertificateInstaller;
-import qz.utils.*;
import javax.swing.*;
-import java.io.File;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -60,87 +50,22 @@ public class PrintSocketServer {
private static final AtomicInteger securePortIndex = new AtomicInteger(0);
private static final AtomicInteger insecurePortIndex = new AtomicInteger(0);
+ private static final AtomicBoolean running = new AtomicBoolean(false);
private static TrayManager trayManager;
- private static CertificateManager certificateManager;
+ private static Server server;
- private static boolean forceHeadless;
+ public static void runServer(CertificateManager certManager, boolean headless) throws InterruptedException, InvocationTargetException {
+ SwingUtilities.invokeAndWait(() -> {
+ PrintSocketServer.setTrayManager(new TrayManager(headless));
+ });
- public static void main(String[] args) {
- ArgParser parser = new ArgParser(args);
- if (parser.intercept()) {
- System.exit(parser.getExitCode());
- }
- forceHeadless = parser.hasFlag(ArgValue.HEADLESS);
- SingleInstanceChecker.stealWebsocket = parser.hasFlag(STEAL);
-
- log.info(Constants.ABOUT_TITLE + " version: {}", Constants.VERSION);
- log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY);
- log.info("Java version: {}", Constants.JAVA_VERSION.toString());
- log.info("Java vendor: {}", Constants.JAVA_VENDOR);
- setupFileLogging();
-
- try {
- // Gets and sets the SSL info, properties file
- certificateManager = Installer.getInstance().certGen(false);
- // Reoccurring (e.g. hourly) cert expiration check
- new ExpiryTask(certificateManager).schedule();
- }
- catch(Exception e) {
- log.error("Something went critically wrong loading HTTPS", e);
- }
- Installer.getInstance().addUserSettings();
-
- // Load overridable preferences set in qz-tray.properties file
- NetworkUtilities.setPreferences(certificateManager.getProperties());
- SingleInstanceChecker.setPreferences(certificateManager.getProperties());
-
- // Linux needs the cert installed in user-space on every launch for Chrome SSL to work
- if (!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
- NativeCertificateInstaller.getInstance().install(certificateManager.getKeyPair(KeyPairWrapper.Type.CA).getCert());
- }
-
- try {
- log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
- SwingUtilities.invokeAndWait(() -> trayManager = new TrayManager(forceHeadless));
- runServer();
- }
- catch(Exception e) {
- log.error("Could not start tray manager", e);
- }
-
- log.warn("The web socket server is no longer running");
- }
-
- private static void setupFileLogging() {
- FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
- rollingPolicy.setFileNamePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log.%i");
- rollingPolicy.setMaxIndex(Constants.LOG_ROTATIONS);
-
- SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy(Constants.LOG_SIZE);
-
- RollingFileAppender fileAppender = new RollingFileAppender();
- fileAppender.setLayout(new PatternLayout("%d{ISO8601} [%p] %m%n"));
- fileAppender.setThreshold(Level.DEBUG);
- fileAppender.setFile(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log");
- fileAppender.setRollingPolicy(rollingPolicy);
- fileAppender.setTriggeringPolicy(triggeringPolicy);
- fileAppender.setEncoding("UTF-8");
-
- fileAppender.setImmediateFlush(true);
- fileAppender.activateOptions();
-
- org.apache.log4j.Logger.getRootLogger().addAppender(fileAppender);
- }
-
- public static void runServer() {
- Server server = findAvailableSecurePort();
+ server = findAvailableSecurePort(certManager);
Connector secureConnector = null;
if (server.getConnectors().length > 0 && !server.getConnectors()[0].isFailed()) {
secureConnector = server.getConnectors()[0];
}
- final AtomicBoolean running = new AtomicBoolean(false);
while(!running.get() && insecurePortIndex.get() < INSECURE_PORTS.size()) {
try {
ServerConnector connector = new ServerConnector(server);
@@ -160,8 +85,9 @@ public static void runServer() {
filter.getFactory().getPolicy().setMaxTextMessageSize(MAX_MESSAGE_SIZE);
// Handle HTTP landing page
- ServletHolder httpServlet = new ServletHolder(new HttpAboutServlet(certificateManager));
+ ServletHolder httpServlet = new ServletHolder(new HttpAboutServlet(certManager));
httpServlet.setInitParameter("resourceBase", "/");
+
context.addServlet(httpServlet, "/");
context.addServlet(httpServlet, "/json");
@@ -169,10 +95,24 @@ public static void runServer() {
server.setStopAtShutdown(true);
server.start();
+ trayManager.setReloadThread(new Thread(() -> {
+ try {
+ trayManager.setDangerIcon();
+ running.set(false);
+ securePortIndex.set(0);
+ insecurePortIndex.set(0);
+ server.stop();
+ }
+ catch(Exception e) {
+ log.error("Failed to reload", e);
+ trayManager.displayErrorMessage("Error stopping print socket: " + e.getLocalizedMessage());
+ }
+ }));
+
running.set(true);
- log.info("Server started on port(s) " + TrayManager.getPorts(server));
- trayManager.setServer(server, running, securePortIndex, insecurePortIndex);
+ log.info("Server started on port(s) " + getPorts(server));
+ trayManager.setServer(server, insecurePortIndex.get());
server.join();
}
catch(IOException | MultiException e) {
@@ -192,19 +132,19 @@ public static void runServer() {
}
}
- private static Server findAvailableSecurePort() {
+ private static Server findAvailableSecurePort(CertificateManager certManager) {
Server server = new Server();
- if (certificateManager != null) {
+ if (certManager != null) {
final AtomicBoolean runningSecure = new AtomicBoolean(false);
while(!runningSecure.get() && securePortIndex.get() < SECURE_PORTS.size()) {
try {
// Bind the secure socket on the proper port number (i.e. 8181), add it as an additional connector
- SslConnectionFactory sslConnection = new SslConnectionFactory(certificateManager.configureSslContextFactory(), HttpVersion.HTTP_1_1.asString());
+ SslConnectionFactory sslConnection = new SslConnectionFactory(certManager.configureSslContextFactory(), HttpVersion.HTTP_1_1.asString());
HttpConnectionFactory httpConnection = new HttpConnectionFactory(new HttpConfiguration());
ServerConnector secureConnector = new ServerConnector(server, sslConnection, httpConnection);
- secureConnector.setHost(certificateManager.getProperties().getProperty("wss.host"));
+ secureConnector.setHost(certManager.getProperties().getProperty("wss.host"));
secureConnector.setPort(getSecurePortInUse());
server.setConnectors(new Connector[] {secureConnector});
@@ -237,15 +177,27 @@ private static Server findAvailableSecurePort() {
return server;
}
- /**
- * Get the TrayManager instance for this SocketServer
- *
- * @return The TrayManager instance
- */
+ public static void setTrayManager(TrayManager manager) {
+ trayManager = manager;
+ }
+
public static TrayManager getTrayManager() {
return trayManager;
}
+ public static Server getServer() {
+ return server;
+ }
+
+ public static AtomicBoolean getRunning() {
+ return running;
+ }
+
+ @Deprecated
+ public static void main(String ... args) {
+ App.main(args);
+ }
+
public static int getSecurePortInUse() {
return SECURE_PORTS.get(securePortIndex.get());
}
@@ -254,8 +206,20 @@ public static int getInsecurePortInUse() {
return INSECURE_PORTS.get(insecurePortIndex.get());
}
- public static Properties getTrayProperties() {
- return certificateManager == null? null:certificateManager.getProperties();
+ /**
+ * Returns a String representation of the ports assigned to the specified Server
+ */
+ public static String getPorts(Server server) {
+ StringBuilder ports = new StringBuilder();
+ for(Connector c : server.getConnectors()) {
+ if (ports.length() > 0) {
+ ports.append(", ");
+ }
+
+ ports.append(((ServerConnector)c).getLocalPort());
+ }
+
+ return ports.toString();
}
}
diff --git a/test/qz/installer/browser/AppFinderTests.java b/test/qz/installer/browser/AppFinderTests.java
index 1537e1788..5c3a9dd75 100644
--- a/test/qz/installer/browser/AppFinderTests.java
+++ b/test/qz/installer/browser/AppFinderTests.java
@@ -2,7 +2,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import qz.installer.Installer;
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
import qz.installer.certificate.firefox.locator.AppAlias;
import qz.installer.certificate.firefox.locator.AppInfo;