From fb4e0d6de08e2a0dcf5bba8ac882411cfd54c4ca Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 13 Oct 2023 00:56:12 -0400 Subject: [PATCH 1/2] Add provisioning support through a JSON file. Closes #1220 --- .gitignore | 1 + ant/apple/installer.xml | 15 + ant/project.properties | 4 + build.xml | 38 ++- src/log4j2.xml | 1 + .../jdesktop/swinghelper/tray/JXTrayIcon.java | 3 +- src/qz/App.java | 10 + src/qz/build/JLink.java | 6 +- src/qz/build/jlink/Arch.java | 26 -- src/qz/build/jlink/Platform.java | 2 +- src/qz/build/jlink/Url.java | 1 + src/qz/build/jlink/Vendor.java | 4 +- src/qz/build/provision/ProvisionBuilder.java | 233 ++++++++++++++ src/qz/build/provision/Step.java | 303 ++++++++++++++++++ src/qz/build/provision/params/Arch.java | 70 ++++ src/qz/build/provision/params/EnumParser.java | 64 ++++ src/qz/build/provision/params/Os.java | 67 ++++ src/qz/build/provision/params/Phase.java | 19 ++ src/qz/build/provision/params/Type.java | 21 ++ .../build/provision/params/types/Remover.java | 67 ++++ .../build/provision/params/types/Script.java | 36 +++ .../provision/params/types/Software.java | 62 ++++ src/qz/common/AboutInfo.java | 2 +- src/qz/common/Constants.java | 2 + src/qz/common/PropertyHelper.java | 2 +- src/qz/common/TrayManager.java | 2 +- src/qz/installer/Installer.java | 49 ++- src/qz/installer/MacInstaller.java | 2 - .../certificate/CertificateManager.java | 10 +- .../NativeCertificateInstaller.java | 2 +- .../firefox/locator/AppLocator.java | 2 +- .../provision/ProvisionInstaller.java | 148 +++++++++ .../provision/invoker/CertInvoker.java | 26 ++ .../provision/invoker/Invokable.java | 10 + .../provision/invoker/InvokableResource.java | 63 ++++ .../provision/invoker/PropertyInvoker.java | 85 +++++ .../provision/invoker/RemoverInvoker.java | 100 ++++++ .../provision/invoker/ScriptInvoker.java | 77 +++++ .../provision/invoker/SoftwareInvoker.java | 87 +++++ src/qz/printer/action/html/WebApp.java | 2 +- src/qz/printer/info/NativePrinterMap.java | 2 +- src/qz/utils/ArgParser.java | 55 +++- src/qz/utils/ArgValue.java | 2 + src/qz/utils/ConnectionUtilities.java | 21 +- src/qz/utils/FileUtilities.java | 26 +- src/qz/utils/LibUtilities.java | 15 +- src/qz/utils/ShellUtilities.java | 79 +++-- src/qz/utils/SystemUtilities.java | 81 +---- src/qz/utils/WindowsUtilities.java | 3 +- .../provision/ProvisionerInstallerTests.java | 20 ++ .../installer/provision/resources/cert1.crt | 60 ++++ .../provision/resources/provision.json | 117 +++++++ .../installer/provision/resources/script1.ps1 | 5 + test/qz/installer/provision/resources/script2 | 9 + .../installer/provision/resources/script3.sh | 7 + .../installer/provision/resources/script4.py | 15 + 56 files changed, 2072 insertions(+), 169 deletions(-) create mode 100644 src/qz/build/provision/ProvisionBuilder.java create mode 100644 src/qz/build/provision/Step.java create mode 100644 src/qz/build/provision/params/Arch.java create mode 100644 src/qz/build/provision/params/EnumParser.java create mode 100644 src/qz/build/provision/params/Os.java create mode 100644 src/qz/build/provision/params/Phase.java create mode 100644 src/qz/build/provision/params/Type.java create mode 100644 src/qz/build/provision/params/types/Remover.java create mode 100644 src/qz/build/provision/params/types/Script.java create mode 100644 src/qz/build/provision/params/types/Software.java create mode 100644 src/qz/installer/provision/ProvisionInstaller.java create mode 100644 src/qz/installer/provision/invoker/CertInvoker.java create mode 100644 src/qz/installer/provision/invoker/Invokable.java create mode 100644 src/qz/installer/provision/invoker/InvokableResource.java create mode 100644 src/qz/installer/provision/invoker/PropertyInvoker.java create mode 100644 src/qz/installer/provision/invoker/RemoverInvoker.java create mode 100644 src/qz/installer/provision/invoker/ScriptInvoker.java create mode 100644 src/qz/installer/provision/invoker/SoftwareInvoker.java create mode 100644 test/qz/installer/provision/ProvisionerInstallerTests.java create mode 100644 test/qz/installer/provision/resources/cert1.crt create mode 100644 test/qz/installer/provision/resources/provision.json create mode 100644 test/qz/installer/provision/resources/script1.ps1 create mode 100644 test/qz/installer/provision/resources/script2 create mode 100644 test/qz/installer/provision/resources/script3.sh create mode 100644 test/qz/installer/provision/resources/script4.py diff --git a/.gitignore b/.gitignore index 6d505cf44..7fc0368b4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ /.idea/compiler.xml /fx.zip /README.md +/provision.json .DS_Store windows-debug-launcher.nsi.in \ No newline at end of file diff --git a/ant/apple/installer.xml b/ant/apple/installer.xml index 6fbc96401..1d9d8e014 100644 --- a/ant/apple/installer.xml +++ b/ant/apple/installer.xml @@ -199,6 +199,21 @@ + + + + + + + + + + + + + + + diff --git a/ant/project.properties b/ant/project.properties index 29d758324..40ae19f09 100644 --- a/ant/project.properties +++ b/ant/project.properties @@ -51,6 +51,10 @@ jlink.java.gc.version="gc-ver-is-empty" javafx.version=19_monocle javafx.mirror=https://download2.gluonhq.com/openjfx +# Provisioning +provision.file=${basedir}/provision.json +provision.dir=${dist.dir}/provision + # Mask tray toggle (Apple only) java.mask.tray=true diff --git a/build.xml b/build.xml index fb06c1487..8eddf3e75 100644 --- a/build.xml +++ b/build.xml @@ -42,8 +42,9 @@ - - + + + @@ -240,6 +241,31 @@ The "include-assets" task is deprecated, please use "build-demo" instead. + + + + + + + ${provision.message} + + + + + + + + + + + + + + + + + + @@ -264,10 +290,10 @@ - - - - + + + + \ No newline at end of file diff --git a/src/log4j2.xml b/src/log4j2.xml index 35dd09e9d..22e36d069 100644 --- a/src/log4j2.xml +++ b/src/log4j2.xml @@ -8,6 +8,7 @@ + diff --git a/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java index cbc609806..2e14fb6af 100644 --- a/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java +++ b/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java @@ -19,7 +19,6 @@ package org.jdesktop.swinghelper.tray; -import com.github.zafarkhaja.semver.Version; import qz.common.Constants; import qz.utils.MacUtilities; import qz.utils.SystemUtilities; @@ -162,7 +161,7 @@ public void actionPerformed(ActionEvent e) { @Override public Dimension getSize() { Dimension iconSize = new Dimension(super.getSize()); - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { // macOS icons are slightly smaller than the size reported case MAC: // Handle retina display diff --git a/src/qz/App.java b/src/qz/App.java index 069dd66aa..39f31a81e 100644 --- a/src/qz/App.java +++ b/src/qz/App.java @@ -10,12 +10,14 @@ import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; import org.apache.logging.log4j.core.filter.ThresholdFilter; import org.apache.logging.log4j.core.layout.PatternLayout; +import qz.build.provision.params.Phase; 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.installer.provision.ProvisionInstaller; import qz.utils.*; import qz.ws.PrintSocketServer; import qz.ws.SingleInstanceChecker; @@ -67,6 +69,14 @@ public static void main(String ... args) { } } + // Invoke any provisioning steps that are phase=startup + try { + ProvisionInstaller provisionInstaller = new ProvisionInstaller(SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR)); + provisionInstaller.invoke(Phase.STARTUP); + } catch(Exception e) { + log.warn("An error occurred provisioning \"phase\": \"startup\" entries", e); + } + try { log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION); // Start the WebSocket diff --git a/src/qz/build/JLink.java b/src/qz/build/JLink.java index de45f5482..5e6cc10d8 100644 --- a/src/qz/build/JLink.java +++ b/src/qz/build/JLink.java @@ -15,10 +15,10 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import qz.build.jlink.Arch; import qz.build.jlink.Platform; import qz.build.jlink.Vendor; import qz.build.jlink.Url; +import qz.build.provision.params.Arch; import qz.common.Constants; import qz.utils.*; @@ -60,7 +60,7 @@ public class JLink { public JLink(String targetPlatform, String targetArch, String javaVendor, String javaVersion, String gcEngine, String gcVersion, String targetJdk) throws IOException { this.hostPlatform = Platform.getCurrentPlatform(); - this.hostArch = Arch.getCurrentArch(); + this.hostArch = SystemUtilities.getArch(); this.targetPlatform = Platform.parse(targetPlatform, this.hostPlatform); this.targetArch = Arch.parse(targetArch, this.hostArch); @@ -150,7 +150,7 @@ private String downloadJdk(Arch arch, Platform platform) throws IOException { String url = new Url(this.javaVendor).format(arch, platform, this.gcEngine, this.javaSemver, this.javaVersion, this.gcVersion); // 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.value(), arch.value(), platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url) + String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.value(), arch, platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url) .fetch() .uncompress(); diff --git a/src/qz/build/jlink/Arch.java b/src/qz/build/jlink/Arch.java index 8e41ea518..e69de29bb 100644 --- a/src/qz/build/jlink/Arch.java +++ b/src/qz/build/jlink/Arch.java @@ -1,26 +0,0 @@ -package qz.build.jlink; - -/** - * Handling of architectures - */ -public enum Arch implements Parsable { - AMD64("amd64", "x86_64", "x64"), - AARCH64("aarch64", "arm64"), - ARM32("arm", "arm32", "arm32hf", "aarch32", "aarch32hf"), - RISCV64("riscv", "riscv64"); - - public final String[] matches; - Arch(String ... matches) { this.matches = matches; } - - public static Arch parse(String value, Arch fallback) { - return Parsable.parse(Arch.class, value, fallback); - } - - public static Arch parse(String value) { - return Parsable.parse(Arch.class, value); - } - - public static Arch getCurrentArch() { - return Parsable.parse(Arch.class, System.getProperty("os.arch")); - } -} diff --git a/src/qz/build/jlink/Platform.java b/src/qz/build/jlink/Platform.java index 2271cb9e9..87149ceea 100644 --- a/src/qz/build/jlink/Platform.java +++ b/src/qz/build/jlink/Platform.java @@ -24,7 +24,7 @@ public static Platform parse(String value) { } public static Platform getCurrentPlatform() { - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case MAC: return Platform.MAC; case WINDOWS: diff --git a/src/qz/build/jlink/Url.java b/src/qz/build/jlink/Url.java index f8748cfb8..ca98f9b1c 100644 --- a/src/qz/build/jlink/Url.java +++ b/src/qz/build/jlink/Url.java @@ -3,6 +3,7 @@ import com.github.zafarkhaja.semver.Version; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import qz.build.provision.params.Arch; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; diff --git a/src/qz/build/jlink/Vendor.java b/src/qz/build/jlink/Vendor.java index e0401c93e..0b1cdbe9c 100644 --- a/src/qz/build/jlink/Vendor.java +++ b/src/qz/build/jlink/Vendor.java @@ -1,7 +1,7 @@ package qz.build.jlink; import com.github.zafarkhaja.semver.Version; -import qz.build.JLink; +import qz.build.provision.params.Arch; /** * Handling of java vendors @@ -61,7 +61,7 @@ public String getUrlArch(Arch arch) { default: return "arm"; } - case AMD64: + case X86_64: switch(this) { // BellSoft uses "amd64" case BELLSOFT: diff --git a/src/qz/build/provision/ProvisionBuilder.java b/src/qz/build/provision/ProvisionBuilder.java new file mode 100644 index 000000000..b13ec8d30 --- /dev/null +++ b/src/qz/build/provision/ProvisionBuilder.java @@ -0,0 +1,233 @@ +package qz.build.provision; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.build.provision.params.Arch; +import qz.build.provision.params.Os; +import qz.build.provision.params.Type; +import qz.build.provision.params.types.Script; +import qz.build.provision.params.types.Software; +import qz.common.Constants; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ProvisionBuilder { + protected static final Logger log = LogManager.getLogger(ProvisionBuilder.class); + + public static final Path BUILD_PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR); + public static final File BUILD_PROVISION_FILE = BUILD_PROVISION_FOLDER.resolve(Constants.PROVISION_FILE).toFile(); + + private File ingestFile; + private JSONArray jsonSteps; + private Arch targetArch; + private Os targetOs; + + /** + * Parses command line input to create a "provision" folder in the dist directory for customizing the installation or startup + */ + public ProvisionBuilder(String type, String phase, String os, String arch, String data, String args, String description, String ... varArgs) throws IOException, JSONException { + createProvisionDirectory(false); + + targetOs = Os.ALL; + targetArch = Arch.ALL; + jsonSteps = new JSONArray(); + + // Wrap into JSON so that we can save it + JSONObject jsonStep = new JSONObject(); + putPattern(jsonStep, "description", description); + putPattern(jsonStep, "type", type); + putPattern(jsonStep, "phase", phase); + putPattern(jsonStep, "os", os); + putPattern(jsonStep, "arch", arch); + putPattern(jsonStep, "data", data); + putPattern(jsonStep, "args", args); + putPattern(jsonStep, "arg%d", varArgs); + + // Command line invocation, use the working directory + Path relativePath = Paths.get(System.getProperty("user.dir")); + ingestStep(jsonStep, relativePath); + } + + /** + * To be called by ant's provision target + */ + public ProvisionBuilder(File antJsonFile, String antTargetOs, String antTargetArch) throws IOException, JSONException { + createProvisionDirectory(true); + + // Calculate the target os, architecture + this.targetArch = Arch.parseStrict(antTargetArch); + this.targetOs = Os.parseStrict(antTargetOs); + + this.jsonSteps = new JSONArray(); + this.ingestFile = antJsonFile; + + String jsonData = FileUtils.readFileToString(antJsonFile, StandardCharsets.UTF_8); + JSONArray pendingSteps = new JSONArray(jsonData); + + // Cycle through so that each Step can be individually processed + Path relativePath = antJsonFile.toPath().getParent(); + for(int i = 0; i < pendingSteps.length(); i++) { + JSONObject jsonStep = pendingSteps.getJSONObject(i); + System.out.println(); + try { + ingestStep(jsonStep, relativePath); + } catch(Exception e) { + log.warn("[SKIPPED] Step '{}'", jsonStep, e); + } + } + + } + + public JSONArray getJson() { + return jsonSteps; + } + + /** + * Construct as a Step to perform basic parsing/sanity checks + * Copy resources (if needed) to provisioning directory + */ + private void ingestStep(JSONObject jsonStep, Path relativePath) throws JSONException, IOException { + Step step = Step.parse(jsonStep, relativePath); + if(!targetOs.matches(step.os)) { + log.info("[SKIPPED] Os '{}' does not match target Os '{}' '{}'", Os.serialize(step.os), targetOs, step); + return; + } + + if(!targetArch.matches(step.arch)) { + log.info("[SKIPPED] Arch '{}' does not match target Os '{}' '{}'", Arch.serialize(step.arch), targetArch, step); + return; + } + + if(copyResource(step)) { + log.info("[SUCCESS] Step successfully processed '{}'", step); + jsonSteps.put(step.toJSON()); + } else { + log.error("[SKIPPED] Resources could not be saved '{}'", step); + } + } + + /** + * Save any resources files required for INSTALL and SCRIPT steps to provision folder + */ + public boolean copyResource(Step step) throws IOException { + switch(step.getType()) { + case CERT: + case SCRIPT: + case SOFTWARE: + boolean isRelative = !Paths.get(step.getData()).isAbsolute(); + File src; + if(isRelative) { + if(ingestFile != null) { + Path parentDir = ingestFile.getParentFile().toPath(); + src = parentDir.resolve(step.getData()).toFile(); + } else { + throw formatted("Unable to resolve path: '%s' '%s'", step.getData(), step); + } + } else { + src = new File(step.getData()); + } + String fileName = src.getName(); + if(fileName.equals(BUILD_PROVISION_FILE.getName())) { + throw formatted("Resource name conflicts with provision file '%s' '%s'", fileName, step); + } + File dest = BUILD_PROVISION_FOLDER.resolve(fileName).toFile(); + int i = 0; + // Avoid conflicting file names + String name = dest.getName(); + + // Avoid resource clobbering when being invoked by command line or providing certificates. + // Otherwise, assume the intent is to re-use the same resource (e.g. "my_script.sh", etc) + if(ingestFile == null || step.getType() == Type.CERT) { + while(dest.exists()) { + // Append "filename-1.txt" until there's no longer a conflict + if (name.contains(".")) { + dest = BUILD_PROVISION_FOLDER.resolve(String.format("%s-%s.%s", FilenameUtils.removeExtension(name), ++i, + FilenameUtils.getExtension(name))).toFile(); + } else { + dest = BUILD_PROVISION_FOLDER.resolve(String.format("%-%", name, ++i)).toFile(); + } + } + } + + FileUtils.copyFile(src, dest); + if(dest.exists()) { + step.setData(BUILD_PROVISION_FOLDER.relativize(dest.toPath()).toString()); + } else { + return false; + } + break; + default: + } + return true; + } + + /** + * Appends the JSONObject to the end of the provisionFile + */ + public boolean saveJson(boolean overwrite) throws IOException, JSONException { + // Read existing JSON file if exists + JSONArray mergeSteps; + if(!overwrite && BUILD_PROVISION_FILE.exists()) { + String jsonData = FileUtils.readFileToString(BUILD_PROVISION_FILE, StandardCharsets.UTF_8); + mergeSteps = new JSONArray(jsonData); + } else { + mergeSteps = new JSONArray(); + } + + // Merge in new steps + for(int i = 0; i < jsonSteps.length(); i++) { + mergeSteps.put(jsonSteps.getJSONObject(i)); + } + + FileUtils.writeStringToFile(BUILD_PROVISION_FILE, mergeSteps.toString(3), StandardCharsets.UTF_8); + return true; + } + + /** + * Convenience method for adding a name/value pair into the JSONObject + */ + private static void putPattern(JSONObject jsonStep, String name, String val) throws JSONException { + if(val != null && !val.isEmpty()) { + jsonStep.put(name, val); + } + } + + /** + * Convenience method for adding consecutive patterned value pairs into the JSONObject + * e.g. --arg1 "foo" --arg2 "bar" + */ + private static void putPattern(JSONObject jsonStep, String pattern, String ... varArgs) throws JSONException { + int argCounter = 0; + for(String arg : varArgs) { + jsonStep.put(String.format(pattern, ++argCounter), arg); + } + } + + private static void createProvisionDirectory(boolean cleanDirectory) throws IOException { + if(cleanDirectory) { + FileUtils.deleteDirectory(BUILD_PROVISION_FOLDER.toFile()); + } + if(BUILD_PROVISION_FOLDER.toFile().isDirectory()) { + return; + } + if(BUILD_PROVISION_FOLDER.toFile().mkdirs()) { + return; + } + throw formatted("Could not create provision destination: '%'", BUILD_PROVISION_FOLDER); + } + + private static IOException formatted(String message, Object ... args) { + String formatted = String.format(message, args); + return new IOException(formatted); + } +} diff --git a/src/qz/build/provision/Step.java b/src/qz/build/provision/Step.java new file mode 100644 index 000000000..6c73ce661 --- /dev/null +++ b/src/qz/build/provision/Step.java @@ -0,0 +1,303 @@ +package qz.build.provision; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.build.provision.params.Arch; +import qz.build.provision.params.Os; +import qz.build.provision.params.Phase; +import qz.build.provision.params.Type; +import qz.build.provision.params.types.Remover; +import qz.build.provision.params.types.Software; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class Step { + protected static final Logger log = LogManager.getLogger(Step.class); + + String description; + Type type; + List args; // Type.SCRIPT or Type.INSTALLER only + HashSet os; + HashSet arch; + Phase phase; + String data; + + Path relativePath; + Class relativeClass; + + public Step(Path relativePath, String description, Type type, HashSet os, HashSet arch, Phase phase, String data, List args) { + this.relativePath = relativePath; + this.description = description; + this.type = type; + this.os = os; + this.arch = arch; + this.phase = phase; + this.data = data; + this.args = args; + } + + /** + * Only should be used by unit tests + */ + Step(Class relativeClass, String description, Type type, HashSet os, HashSet arch, Phase phase, String data, List args) { + this.relativeClass = relativeClass; + this.description = description; + this.type = type; + this.os = os; + this.arch = arch; + this.phase = phase; + this.data = data; + this.args = args; + } + + @Override + public String toString() { + return "Step { " + + "description=\"" + description + "\", " + + "type=\"" + type + "\", " + + "os=\"" + Os.serialize(os) + "\", " + + "arch=\"" + Arch.serialize(arch) + "\", " + + "phase=\"" + phase + "\", " + + "data=\"" + data + "\", " + + "args=\"" + StringUtils.join(args, ",") + "\" " + + "}"; + } + + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("description", description) + .put("type", type) + .put("os", Os.serialize(os)) + .put("arch", Arch.serialize(arch)) + .put("phase", phase) + .put("data", data); + + for(int i = 0; i < args.size(); i++) { + json.put(String.format("arg%s", i + 1), args.get(i)); + } + return json; + } + + public String getDescription() { + return description; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + + public HashSet getOs() { + return os; + } + + public void setOs(HashSet os) { + this.os = os; + } + + public HashSet getArch() { + return arch; + } + + public void setArch(HashSet arch) { + this.arch = arch; + } + + public Phase getPhase() { + return phase; + } + + public void setPhase(Phase phase) { + this.phase = phase; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public Class getRelativeClass() { + return relativeClass; + } + + public Path getRelativePath() { + return relativePath; + } + + public boolean usingClass() { + return relativeClass != null; + } + + public boolean usingPath() { + return relativePath != null; + } + + public static Step parse(JSONObject jsonStep, Object relativeObject) { + String description = jsonStep.optString("description", ""); + Type type = Type.parse(jsonStep.optString("type", null)); + String data = jsonStep.optString("data", null); + + // Handle installer args + List args = new LinkedList<>(); + if(type == Type.SOFTWARE) { + // Handle space-delimited args + args = Software.parseArgs(jsonStep.optString("args", "")); + // Handle standalone single args (won't break on whitespace) + // e.g. "arg1": "C:\Program Files\Foo" + int argCounter = 0; + while(true) { + String singleArg = jsonStep.optString(String.format("arg%d", ++argCounter), ""); + if(!singleArg.trim().isEmpty()) { + args.add(singleArg.trim()); + } + // stop searching if the next incremental arg (e.g. "arg2") isn't found + break; + } + } + + HashSet os = new HashSet<>(); + if(jsonStep.has("os")) { + // Do not tolerate bad os values + String osString = jsonStep.optString("os"); + os = Os.parse(osString); + if(os.size() == 0) { + throw formatted("Os provided '%s' could not be parsed", osString); + } + } + + HashSet arch = new HashSet<>(); + if(jsonStep.has("arch")) { + // Do not tolerate bad arch values + String archString = jsonStep.optString("arch"); + arch = Arch.parse(archString); + if(arch.size() == 0) { + throw formatted("Arch provided \"%s\" could not be parsed", archString); + } + } + + Phase phase = null; + if(jsonStep.has("phase")) { + String phaseString = jsonStep.optString("phase", null); + phase = Phase.parse(phaseString); + if(phase == null) { + log.warn("Phase provided \"{}\" could not be parsed", phaseString); + } + } + Step step; + if(relativeObject instanceof Path) { + step = new Step((Path)relativeObject, description, type, os, arch, phase, data, args); + } else if(relativeObject instanceof Class) { + step = new Step((Class)relativeObject, description, type, os, arch, phase, data, args); + } else { + throw formatted("Parameter relativeObject must be of type 'Path' or 'Class' but '%s' was provided", relativeObject.getClass()); + } + return step.sanitize(); + } + + private Step sanitize() { + return throwIfNull("Type", type) + .throwIfNull("Data", data) + .validateOs() + .validateArch() + .enforcePhase(Type.PREFERENCE, Phase.STARTUP) + .enforcePhase(Type.CERT, Phase.STARTUP) + .enforcePhase(Type.SOFTWARE, Phase.INSTALL) + .enforcePhase(Type.REMOVER, Phase.INSTALL) + .enforcePhase(Type.PROPERTY, Phase.CERTGEN) + .validateRemover(); + } + + private Step validateRemover() { + if(type != Type.REMOVER) { + return this; + } + Remover remover = Remover.parse(data); + switch(remover) { + case CUSTOM: + break; + default: + if(remover.matchesCurrentSystem()) { + throw formatted("Remover '%s' would conflict with this installer, skipping. ", remover); + } + return this; + } + + // Custom removers must have three elements + if(data == null || data.split(",").length != 3) { + throw formatted("Remover data '%s' is invalid. Data must match a known type [%s] or contain exactly 3 elements.", data, Remover.valuesDelimited(",")); + } + return this; + } + + private Step throwIfNull(String name, Object value) { + if(value == null) { + throw formatted("%s cannot be null", name); + } + return this; + } + + private Step validateOs() { + if(os == null) { + if(type == Type.SOFTWARE) { + // Software must default to a sane operating system + os = Software.parse(data).defaultOs(); + } else { + os = new HashSet<>(); + } + } + if(os.size() == 0) { + os.add(Os.ALL); + log.debug("Os list is null, assuming '{}'", Os.ALL); + } + return this; + } + + private Step validateArch() { + if(arch == null) { + arch = new HashSet<>(); + } + if(arch.size() == 0) { + arch.add(Arch.ALL); + log.debug("Arch list is null, assuming '{}'", Arch.ALL); + } + return this; + } + + private Step enforcePhase(Type matchType, Phase requiredPhase) { + if(type == matchType) { + if(phase == null) { + phase = requiredPhase; + log.debug("Phase is null, defaulting to '{}' based on Type '{}'", phase, type); + } else if (phase != requiredPhase) { + log.debug("Phase '{}' is unsupported for Type '{}', defaulting to '{}'", phase, type, + phase = requiredPhase); + } + } + return this; + } + + private static UnsupportedOperationException formatted(String message, Object ... args) { + String formatted = String.format(message, args); + return new UnsupportedOperationException(formatted); + } +} diff --git a/src/qz/build/provision/params/Arch.java b/src/qz/build/provision/params/Arch.java new file mode 100644 index 000000000..4f8e6620d --- /dev/null +++ b/src/qz/build/provision/params/Arch.java @@ -0,0 +1,70 @@ +package qz.build.provision.params; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; + +/** + * Basic architecture parser + * + * Note: All aliases must be lowercase + */ +public enum Arch { + X86("x32", "i386", "i486", "i586", "i686"), + X86_64("amd64"), + ARM32("arm", "armv1", "armv2", "armv3", "armv4", "armv5", "armv6", "armv7"), + AARCH64("arm64", "armv8", "armv9"), + RISCV32("rv32"), + RISCV64("rv64"), + PPC64("powerpc", "powerpc64"), + ALL(), // special handling + UNKNOWN(); + + private HashSet aliases = new HashSet<>(); + Arch(String ... aliases) { + this.aliases.add(name().toLowerCase(Locale.ENGLISH)); + this.aliases.addAll(Arrays.asList(aliases)); + } + + public static Arch parseStrict(String input) throws UnsupportedOperationException { + return EnumParser.parseStrict(Arch.class, input, ALL, UNKNOWN); + } + + public static HashSet parse(String input) { + return EnumParser.parseSet(Arch.class, Arch.ALL, input); + } + + public static Arch parse(String input, Arch fallback) { + Arch found = bestMatch(input); + return found == UNKNOWN ? fallback : found; + } + + public static Arch bestMatch(String input) { + if(input != null) { + for(Arch arch : values()) { + if (arch.aliases.contains(input.toLowerCase())) { + return arch; + } + } + } + return Arch.UNKNOWN; + } + + public boolean matches(HashSet archList) { + return this == ALL || archList.contains(ALL) || (this != UNKNOWN && archList.contains(this)); + } + + public static String serialize(HashSet archList) { + if(archList.contains(ALL)) { + return "*"; + } + return StringUtils.join(archList, "|"); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/src/qz/build/provision/params/EnumParser.java b/src/qz/build/provision/params/EnumParser.java new file mode 100644 index 000000000..5a9dd3336 --- /dev/null +++ b/src/qz/build/provision/params/EnumParser.java @@ -0,0 +1,64 @@ +package qz.build.provision.params; + +import java.util.EnumSet; +import java.util.HashSet; + +public interface EnumParser { + /** + * Basic enum parser + */ + + static > T parse(Class clazz, String s) { + return parse(clazz, s, null); + } + + static > T parse(Class clazz, String s, T fallbackValue) { + if(s != null) { + for(T en : EnumSet.allOf(clazz)) { + if (en.name().equalsIgnoreCase(s)) { + return en; + } + } + } + return fallbackValue; + } + + static > T parseStrict(Class clazz, String s, T ... blocklist) throws UnsupportedOperationException { + if(s != null) { + HashSet matched = parseSet(clazz, null, s); + if (matched.size() == 1) { + T returnVal = matched.iterator().next(); + boolean blocked = false; + for(T block : blocklist) { + if(returnVal == block) { + blocked = true; + break; + } + } + if(!blocked) { + return returnVal; + } + } + } + throw new UnsupportedOperationException(String.format("%s value '%s' failed to match one and only one item", clazz.getSimpleName(), s)); + } + + static > HashSet parseSet(Class clazz, T all, String s) { + HashSet matched = new HashSet<>(); + if(s != null) { + // Handle ALL="*" + if (all != null && s.equals("*")) { + matched.add(all); + } + + String[] parts = s.split("\\|"); + for(String part : parts) { + T parsed = parse(clazz, part); + if (parsed != null) { + matched.add(parsed); + } + } + } + return matched; + } +} diff --git a/src/qz/build/provision/params/Os.java b/src/qz/build/provision/params/Os.java new file mode 100644 index 000000000..7785912cc --- /dev/null +++ b/src/qz/build/provision/params/Os.java @@ -0,0 +1,67 @@ +package qz.build.provision.params; + +import org.apache.commons.lang3.StringUtils; +import qz.utils.SystemUtilities; + +import java.util.*; + +/** + * Basic OS parser + */ +public enum Os { + WINDOWS, + MAC, + LINUX, + SOLARIS, // unsupported + ALL, // special handling + UNKNOWN; + + public boolean matches(HashSet osList) { + return this == ALL || osList.contains(ALL) || (this != UNKNOWN && osList.contains(this)); + } + + public static boolean matchesHost(HashSet osList) { + for(Os os : osList) { + if(os == SystemUtilities.getOs() || os == ALL) { + return true; + } + } + return false; + } + + public static Os parseStrict(String input) throws UnsupportedOperationException { + return EnumParser.parseStrict(Os.class, input, ALL, UNKNOWN); + } + + public static Os bestMatch(String input) { + if(input != null) { + String name = input.toLowerCase(Locale.ENGLISH); + if (name.contains("win")) { + return Os.WINDOWS; + } else if (name.contains("mac")) { + return Os.MAC; + } else if (name.contains("linux")) { + return Os.LINUX; + } else if (name.contains("sunos")) { + return Os.SOLARIS; + } + } + return Os.UNKNOWN; + } + + public static HashSet parse(String input) { + return EnumParser.parseSet(Os.class, Os.ALL, input); + } + + public static String serialize(HashSet osList) { + if(osList.contains(ALL)) { + return "*"; + } + return StringUtils.join(osList, "|"); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} \ No newline at end of file diff --git a/src/qz/build/provision/params/Phase.java b/src/qz/build/provision/params/Phase.java new file mode 100644 index 000000000..382f1192a --- /dev/null +++ b/src/qz/build/provision/params/Phase.java @@ -0,0 +1,19 @@ +package qz.build.provision.params; + +import java.util.Locale; + +public enum Phase { + INSTALL, + CERTGEN, + STARTUP, + UNINSTALL; + + public static Phase parse(String input) { + return EnumParser.parse(Phase.class, input); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/src/qz/build/provision/params/Type.java b/src/qz/build/provision/params/Type.java new file mode 100644 index 000000000..ddc7fc8bc --- /dev/null +++ b/src/qz/build/provision/params/Type.java @@ -0,0 +1,21 @@ +package qz.build.provision.params; + +import java.util.Locale; + +public enum Type { + SCRIPT, + SOFTWARE, + REMOVER, // QZ Tray remover + CERT, + PROPERTY, + PREFERENCE; + + public static Type parse(String input) { + return EnumParser.parse(Type.class, input); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/src/qz/build/provision/params/types/Remover.java b/src/qz/build/provision/params/types/Remover.java new file mode 100644 index 000000000..7b8b635f8 --- /dev/null +++ b/src/qz/build/provision/params/types/Remover.java @@ -0,0 +1,67 @@ +package qz.build.provision.params.types; + +import org.apache.commons.lang3.StringUtils; +import qz.build.provision.params.EnumParser; +import qz.common.Constants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +public enum Remover { + QZ("QZ Tray", "qz-tray", "qz"), + CUSTOM(null, null, null); // reserved + + private String aboutTitle; + private String propsFile; + private String dataDir; + + Remover(String aboutTitle, String propsFile, String dataDir) { + this.aboutTitle = aboutTitle; + this.propsFile = propsFile; + this.dataDir = dataDir; + } + + public String getAboutTitle() { + return aboutTitle; + } + + public String getPropsFile() { + return propsFile; + } + + public String getDataDir() { + return dataDir; + } + + public static String valuesDelimited(String delimiter) { + ArrayList listing = new ArrayList<>(Arrays.asList(values())); + listing.remove(CUSTOM); + return StringUtils.join(listing, delimiter).toLowerCase(Locale.ENGLISH); + } + + /** + * Defaults to custom if not found + */ + public static Remover parse(String input) { + Remover remover = EnumParser.parse(Remover.class, input); + if(remover == CUSTOM) { + throw new UnsupportedOperationException("Remover 'custom' is reserved for internal purposes"); + } + if(remover == null) { + remover = CUSTOM; + } + return remover; + } + + public boolean matchesCurrentSystem() { + return Constants.ABOUT_TITLE.equals(aboutTitle) || + Constants.PROPS_FILE.equals(propsFile) || + Constants.DATA_DIR.equals(dataDir); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/src/qz/build/provision/params/types/Script.java b/src/qz/build/provision/params/types/Script.java new file mode 100644 index 000000000..1f7eeaa67 --- /dev/null +++ b/src/qz/build/provision/params/types/Script.java @@ -0,0 +1,36 @@ +package qz.build.provision.params.types; + +import qz.build.provision.params.EnumParser; + +import java.nio.file.Path; +import java.util.Locale; + +public enum Script { + PS1, + BAT, + SH, + PY, + RB; + + public static Script parse(String input) { + if(input != null) { + if(input.contains(".")) { + String extension = input.substring(input.lastIndexOf(".") + 1); + return EnumParser.parse(Script.class, extension); + } else { + // If no file extension, assume a shell script + return SH; + } + } + return null; + } + + public static Script parse(Path path) { + return parse(path.toAbsolutePath().toString()); + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} \ No newline at end of file diff --git a/src/qz/build/provision/params/types/Software.java b/src/qz/build/provision/params/types/Software.java new file mode 100644 index 000000000..d96f2015d --- /dev/null +++ b/src/qz/build/provision/params/types/Software.java @@ -0,0 +1,62 @@ +package qz.build.provision.params.types; + +import qz.build.provision.params.EnumParser; +import qz.build.provision.params.Os; + +import java.nio.file.Path; +import java.util.*; + +public enum Software { + EXE, + MSI, + PKG, + DMG, + RUN, + UNKNOWN; + + public static Software parse(String input) { + return EnumParser.parse(Software.class, input, UNKNOWN); + } + + public static Software parse(Path path) { + return parse(path.toString()); + } + + public static List parseArgs(String input) { + List args = new LinkedList<>(); + if(input != null) { + String[] parts = input.split(" "); + for(String part : parts) { + if(!part.trim().isEmpty()) { + args.add(part.trim()); + } + } + } + return args; + } + + public HashSet defaultOs() { + HashSet list = new HashSet<>(); + switch(this) { + case EXE: + case MSI: + list.add(Os.WINDOWS); + break; + case PKG: + case DMG: + list.add(Os.MAC); + break; + case RUN: + list.add(Os.LINUX); + break; + default: + list.add(Os.ALL); + } + return list; + } + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/src/qz/common/AboutInfo.java b/src/qz/common/AboutInfo.java index 14d0a3fe2..775202745 100644 --- a/src/qz/common/AboutInfo.java +++ b/src/qz/common/AboutInfo.java @@ -92,7 +92,7 @@ private static JSONObject environment() throws JSONException { environment .put("os", SystemUtilities.getOsDisplayName()) .put("os version", SystemUtilities.getOsDisplayVersion()) - .put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getJreArch().toString().toLowerCase())) + .put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getArch().toString().toLowerCase())) .put("java (location)", System.getProperty("java.home")) .put("java (vendor)", Constants.JAVA_VENDOR) .put("uptime", DurationFormatUtils.formatDurationWords(uptime, true, false)) diff --git a/src/qz/common/Constants.java b/src/qz/common/Constants.java index 7ba5fe1dd..ad8f7174f 100644 --- a/src/qz/common/Constants.java +++ b/src/qz/common/Constants.java @@ -74,6 +74,8 @@ public class Constants { public static final String OVERRIDE_CERT = "override.crt"; public static final String WHITELIST_CERT_DIR = "whitelist"; + public static final String PROVISION_DIR = "provision"; + public static final String PROVISION_FILE = "provision.json"; public static final String SIGNING_PRIVATE_KEY = "private-key.pem"; public static final String SIGNING_CERTIFICATE = "digital-certificate.txt"; diff --git a/src/qz/common/PropertyHelper.java b/src/qz/common/PropertyHelper.java index ad1fe60f6..aa3fc6f9e 100644 --- a/src/qz/common/PropertyHelper.java +++ b/src/qz/common/PropertyHelper.java @@ -13,7 +13,7 @@ * Created by Tres on 12/16/2015. */ public class PropertyHelper extends Properties { - private static final Logger log = LogManager.getLogger(TrayManager.class); + private static final Logger log = LogManager.getLogger(PropertyHelper.class); private String file; /** diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index 12b773ac0..edb860381 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -115,7 +115,7 @@ public TrayManager(boolean isHeadless) { iconCache = new IconCache(); if (SystemUtilities.isSystemTraySupported(headless)) { // UI mode with tray - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case WINDOWS: tray = TrayType.JX.init(iconCache); // Undocumented HiDPI behavior diff --git a/src/qz/installer/Installer.java b/src/qz/installer/Installer.java index bbd089f84..4ae3c8c2a 100644 --- a/src/qz/installer/Installer.java +++ b/src/qz/installer/Installer.java @@ -14,8 +14,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import qz.auth.Certificate; +import qz.build.provision.params.Phase; import qz.installer.certificate.*; import qz.installer.certificate.firefox.FirefoxCertificateInstaller; +import qz.installer.provision.ProvisionInstaller; import qz.utils.FileUtilities; import qz.utils.SystemUtilities; @@ -59,7 +61,7 @@ public enum PrivilegeLevel { public static Installer getInstance() { if(instance == null) { - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case WINDOWS: instance = new WindowsInstaller(); break; @@ -94,13 +96,15 @@ public static void install() throws Exception { getInstance(); log.info("Installing to {}", instance.getDestination()); instance.removeLibs() + .removeProvisioning() .deployApp() .removeLegacyStartup() .removeLegacyFiles() .addSharedDirectory() .addAppLauncher() .addStartupEntry() - .addSystemSettings(); + .addSystemSettings() + .invokeProvisioning(Phase.INSTALL); } public static void uninstall() { @@ -110,7 +114,8 @@ public static void uninstall() { log.info("Uninstalling from {}", instance.getDestination()); instance.removeSharedDirectory() .removeSystemSettings() - .removeCerts(); + .removeCerts() + .invokeProvisioning(Phase.UNINSTALL); } public Installer deployApp() throws IOException { @@ -126,7 +131,10 @@ public Installer deployApp() throws IOException { // Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011 FileUtils.copyDirectory(src.toFile(), dest.toFile(), false); FileUtilities.setPermissionsRecursively(dest, false); - + // Fix permissions for provisioned files + FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ? + dest.resolve("Contents/Resources").resolve(PROVISION_DIR) : + dest.resolve(PROVISION_DIR), false); if(!SystemUtilities.isWindows()) { setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall"); setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE); @@ -302,18 +310,24 @@ public CertificateManager certGen(boolean forceNew, String... hostNames) throws log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e); } + // Add provisioning steps that come after certgen + if(SystemUtilities.isAdmin()) { + invokeProvisioning(Phase.CERTGEN); + } + return certificateManager; } /** * Remove matching certs from user|system, then Firefox */ - public void removeCerts() { + public Installer removeCerts() { // System certs NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance(); instance.remove(instance.find()); // Firefox certs FirefoxCertificateInstaller.uninstall(); + return this; } /** @@ -338,6 +352,31 @@ public Installer addUserSettings() { return instance; } + public Installer invokeProvisioning(Phase phase) { + try { + Path provisionPath = SystemUtilities.isMac() ? + Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) : + Paths.get(getDestination()).resolve(PROVISION_DIR); + ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath); + provisionInstaller.invoke(phase); + } catch(Exception e) { + log.warn("An error occurred deleting provisioning directory \"phase\": \"{}\" entries", phase, e); + } + return this; + } + + public Installer removeProvisioning() { + try { + Path provisionPath = SystemUtilities.isMac() ? + Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) : + Paths.get(getDestination()).resolve(PROVISION_DIR); + FileUtils.deleteDirectory(provisionPath.toFile()); + } catch(Exception e) { + log.warn("An error occurred removing provision directory", e); + } + return this; + } + public static Properties persistProperties(File oldFile, Properties newProps) { if(oldFile.exists()) { Properties oldProps = new Properties(); diff --git a/src/qz/installer/MacInstaller.java b/src/qz/installer/MacInstaller.java index 73b029d2a..3b5b1c7e3 100644 --- a/src/qz/installer/MacInstaller.java +++ b/src/qz/installer/MacInstaller.java @@ -9,7 +9,6 @@ * 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.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -19,7 +18,6 @@ import java.io.File; import java.io.IOException; -import java.nio.file.*; import java.util.HashMap; import java.util.List; diff --git a/src/qz/installer/certificate/CertificateManager.java b/src/qz/installer/certificate/CertificateManager.java index c67674c91..6e69514a2 100644 --- a/src/qz/installer/certificate/CertificateManager.java +++ b/src/qz/installer/certificate/CertificateManager.java @@ -414,9 +414,15 @@ public static Properties loadProperties(KeyPairWrapper... keyPairs) { public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception { Properties props; + if (existing == null) { - props = new Properties(); - props.load(new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties"))); + FileInputStream fis = null; + try { + props = new Properties(); + props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties"))); + } finally { + if(fis != null) fis.close(); + } } else { props = existing; } diff --git a/src/qz/installer/certificate/NativeCertificateInstaller.java b/src/qz/installer/certificate/NativeCertificateInstaller.java index 4b765c450..cbe3178f3 100644 --- a/src/qz/installer/certificate/NativeCertificateInstaller.java +++ b/src/qz/installer/certificate/NativeCertificateInstaller.java @@ -34,7 +34,7 @@ public static NativeCertificateInstaller getInstance() { } public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) { if (instance == null) { - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case WINDOWS: instance = new WindowsCertificateInstaller(type); break; diff --git a/src/qz/installer/certificate/firefox/locator/AppLocator.java b/src/qz/installer/certificate/firefox/locator/AppLocator.java index c70ab0f08..e02d5d713 100644 --- a/src/qz/installer/certificate/firefox/locator/AppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/AppLocator.java @@ -74,7 +74,7 @@ public static AppLocator getInstance() { } private static AppLocator getPlatformSpecificAppLocator() { - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case WINDOWS: return new WindowsAppLocator(); case MAC: diff --git a/src/qz/installer/provision/ProvisionInstaller.java b/src/qz/installer/provision/ProvisionInstaller.java new file mode 100644 index 000000000..0996b594a --- /dev/null +++ b/src/qz/installer/provision/ProvisionInstaller.java @@ -0,0 +1,148 @@ +package qz.installer.provision; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.Phase; +import qz.build.provision.params.types.Script; +import qz.build.provision.params.types.Software; +import qz.common.Constants; +import qz.installer.provision.invoker.*; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; + +import static qz.common.Constants.*; +import static qz.utils.FileUtilities.*; + +public class ProvisionInstaller { + protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class); + private ArrayList steps; + + static { + // Populate variables for scripting environment + ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE, + "APP_VERSION", VERSION, + "APP_ABBREV", PROPS_FILE, + "APP_VENDOR", ABOUT_COMPANY, + "APP_VENDOR_ABBREV", DATA_DIR, + "APP_ARCH", SystemUtilities.getArch(), + "APP_OS", SystemUtilities.getOs(), + "APP_DIR", SystemUtilities.getAppPath(), + "APP_USER_DIR", USER_DIR, + "APP_SHARED_DIR", SHARED_DIR); + } + + public ProvisionInstaller(Path relativePath) throws IOException, JSONException { + this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile()); + } + + public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException { + if(!jsonFile.exists()) { + log.info("Provision file not found '{}', skipping", jsonFile); + this.steps = new ArrayList<>(); + return; + } + this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath); + } + + /** + * Package private for internal testing only + * Assumes files located in ./resources/ subdirectory + */ + ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException { + this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8)); + } + + /** + * Package private for internal testing only + * Assumes files located in ./resources/ subdirectory + */ + ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException { + this.steps = parse(jsonData, relativeClass); + } + + public void invoke(Phase phase) { + for(Step step : this.steps) { + if(phase == null || step.getPhase() == phase) { + try { + invokeStep(step); + } + catch(Exception e) { + log.error("[PROVISION] Provisioning step failed '{}'", step, e); + } + } + } + } + + public void invoke() { + invoke(null); + } + + private static ArrayList parse(String jsonData, Object relativeObject) throws JSONException { + return parse(new JSONArray(jsonData), relativeObject); + } + + private boolean invokeStep(Step step) throws Exception { + if(Os.matchesHost(step.getOs())) { + log.info("[PROVISION] Invoking step '{}'", step.toString()); + } else { + log.info("[PROVISION] Skipping step for different OS '{}'", step.toString()); + return false; + } + + Invokable invoker; + switch(step.getType()) { + case CERT: + invoker = new CertInvoker(step); + break; + case SCRIPT: + invoker = new ScriptInvoker(step); + break; + case SOFTWARE: + invoker = new SoftwareInvoker(step); + break; + case REMOVER: + invoker = new RemoverInvoker(step); + break; + case PREFERENCE: + invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step)); + break; + case PROPERTY: + invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step)); + break; + default: + throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported."); + } + return invoker.invoke(); + } + + private static ArrayList parse(JSONArray jsonArray, Object relativeObject) throws JSONException { + ArrayList steps = new ArrayList<>(); + for(int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonStep = jsonArray.getJSONObject(i); + try { + steps.add(Step.parse(jsonStep, relativeObject)); + } catch(Exception e) { + log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e); + } + } + return steps; + } + + public static boolean shouldBeExecutable(Path path) { + return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN; + } +} diff --git a/src/qz/installer/provision/invoker/CertInvoker.java b/src/qz/installer/provision/invoker/CertInvoker.java new file mode 100644 index 000000000..8e7b32e87 --- /dev/null +++ b/src/qz/installer/provision/invoker/CertInvoker.java @@ -0,0 +1,26 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.Constants; +import qz.utils.FileUtilities; + +import java.io.File; + +import static qz.utils.ArgParser.ExitStatus.*; + +public class CertInvoker extends InvokableResource { + private Step step; + + public CertInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File cert = dataToFile(step); + if(cert == null) { + return false; + } + return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS; + } +} diff --git a/src/qz/installer/provision/invoker/Invokable.java b/src/qz/installer/provision/invoker/Invokable.java new file mode 100644 index 000000000..508e48990 --- /dev/null +++ b/src/qz/installer/provision/invoker/Invokable.java @@ -0,0 +1,10 @@ +package qz.installer.provision.invoker; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public interface Invokable { + Logger log = LogManager.getLogger(Invokable.class); + + boolean invoke() throws Exception; +} diff --git a/src/qz/installer/provision/invoker/InvokableResource.java b/src/qz/installer/provision/invoker/InvokableResource.java new file mode 100644 index 000000000..5a76d54be --- /dev/null +++ b/src/qz/installer/provision/invoker/InvokableResource.java @@ -0,0 +1,63 @@ +package qz.installer.provision.invoker; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.provision.Step; +import qz.build.provision.params.Type; +import qz.common.Constants; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public abstract class InvokableResource implements Invokable { + static final Logger log = LogManager.getLogger(InvokableResource.class); + + public static File dataToFile(Step step) throws IOException { + Path resourcePath = Paths.get(step.getData()); + if(resourcePath.isAbsolute() || step.usingPath()) { + return pathResourceToFile(step); + } + if(step.usingClass()) { + return classResourceToFile(step); + } + return null; + } + + /** + * Resolves the resource directly from file + */ + private static File pathResourceToFile(Step step) { + String resourcePath = step.getData(); + Path dataPath = Paths.get(resourcePath); + return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile(); + } + + /** + * Copies resource from JAR to a temp file for use in installation + */ + private static File classResourceToFile(Step step) throws IOException { + // Resource may be inside the jar + InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData()); + if(in == null) { + log.warn("Resource '{}' is missing, skipping step", step.getData()); + return null; + } + String suffix = "_" + Paths.get(step.getData()).getFileName().toString(); + File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix); + Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + IOUtils.closeQuietly(in); + + // Set scripts executable + if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) { + destination.setExecutable(true, false); + } + return destination; + } +} diff --git a/src/qz/installer/provision/invoker/PropertyInvoker.java b/src/qz/installer/provision/invoker/PropertyInvoker.java new file mode 100644 index 000000000..75691bdfd --- /dev/null +++ b/src/qz/installer/provision/invoker/PropertyInvoker.java @@ -0,0 +1,85 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.Constants; +import qz.common.PropertyHelper; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.AbstractMap; +import java.util.ArrayList; + +public class PropertyInvoker implements Invokable { + private Step step; + PropertyHelper properties; + + public PropertyInvoker(Step step, PropertyHelper properties) { + this.step = step; + this.properties = properties; + } + + public boolean invoke() { + if(step.getData() != null && !step.getData().trim().isEmpty()) { + String[] props = step.getData().split("\\|"); + ArrayList> pairs = new ArrayList<>(); + for(String prop : props) { + AbstractMap.SimpleEntry pair = parsePropertyPair(step, prop); + if (pair != null) { + pairs.add(pair); + } + } + if (!pairs.isEmpty()) { + for(AbstractMap.SimpleEntry pair : pairs) { + properties.setProperty(pair.getKey(), pair.getValue()); + } + if (properties.save()) { + log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType()); + return true; + } + log.error("An error occurred saving properties '{}' to file", step.getData()); + } + } else { + log.error("Skipping Step '{}', Data is null or empty", step.getType()); + } + return false; + } + + public static PropertyHelper getProperties(Step step) { + File propertiesFile; + if(step.getRelativePath() != null) { + // Assume qz-tray.properties is one directory up from provision folder + // required to prevent installing to payload + propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile(); + } else { + // If relative path isn't set, fallback to the jar's parent path + propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile(); + } + log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile); + return new PropertyHelper(propertiesFile); + } + + public static PropertyHelper getPreferences(Step step) { + return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties"); + } + + + private static AbstractMap.SimpleEntry parsePropertyPair(Step step, String prop) { + if(prop.contains("=")) { + String[] pair = prop.split("=", 2); + if (!pair[0].trim().isEmpty()) { + if (!pair[1].trim().isEmpty()) { + return new AbstractMap.SimpleEntry<>(pair[0], pair[1]); + } else { + log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop); + } + } else { + log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop); + } + } else { + log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop); + } + + return null; + } +} diff --git a/src/qz/installer/provision/invoker/RemoverInvoker.java b/src/qz/installer/provision/invoker/RemoverInvoker.java new file mode 100644 index 000000000..3b8271a21 --- /dev/null +++ b/src/qz/installer/provision/invoker/RemoverInvoker.java @@ -0,0 +1,100 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Remover; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; + +public class RemoverInvoker extends InvokableResource { + private Step step; + private String aboutTitle; // e.g. "QZ Tray" + private String propsFile; // e.g. "qz-tray" + private String dataDir; // e.g. "qz" + + + public RemoverInvoker(Step step) { + this.step = step; + Remover remover = Remover.parse(step.getData()); + if(remover == Remover.CUSTOM) { + // Fields are comma delimited in the data field + parseCustomFromData(step.getData()); + } else { + aboutTitle = remover.getAboutTitle(); + propsFile = remover.getPropsFile(); + dataDir = remover.getDataDir(); + } + } + + @Override + public boolean invoke() throws Exception { + ArrayList command = getRemoveCommand(); + if(command.size() == 0) { + log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle); + return true; + } + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()])); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + public void parseCustomFromData(String data) { + String[] parts = data.split(","); + aboutTitle = parts[0].trim(); + propsFile = parts[1].trim(); + dataDir = parts[2].trim(); + } + + /** + * Returns the installer command (including the installer itself and if needed, arguments) to + * invoke the installer file + */ + public ArrayList getRemoveCommand() { + ArrayList removeCmd = new ArrayList<>(); + Os os = SystemUtilities.getOs(); + switch(os) { + case WINDOWS: + Path win = Paths.get(System.getenv("PROGRAMFILES")) + .resolve(aboutTitle) + .resolve("uninstall.exe"); + + if(win.toFile().exists()) { + removeCmd.add(win.toString()); + removeCmd.add("/S"); + break; + } + case MAC: + Path legacy = Paths.get("/Applications") + .resolve(aboutTitle + ".app") + .resolve("Contents") + .resolve("uninstall"); + + Path mac = Paths.get("/Applications") + .resolve(aboutTitle + ".app") + .resolve("Contents") + .resolve("Resources") + .resolve("uninstall"); + + if(legacy.toFile().exists()) { + removeCmd.add(legacy.toString()); + } else if(mac.toFile().exists()) { + removeCmd.add(mac.toString()); + } + break; + default: + Path linux = Paths.get("/opt") + .resolve(propsFile) + .resolve("uninstall"); + if(linux.toFile().exists()) { + removeCmd.add(linux.toString()); + } + } + return removeCmd; + } +} diff --git a/src/qz/installer/provision/invoker/ScriptInvoker.java b/src/qz/installer/provision/invoker/ScriptInvoker.java new file mode 100644 index 000000000..bbd9d9a6d --- /dev/null +++ b/src/qz/installer/provision/invoker/ScriptInvoker.java @@ -0,0 +1,77 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Script; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.ArrayList; + +public class ScriptInvoker extends InvokableResource { + private Step step; + + public ScriptInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File script = dataToFile(step); + if(script == null) { + return false; + } + Script engine = Script.parse(step.getData()); + ArrayList command = getInterpreter(engine); + if(command.isEmpty() && SystemUtilities.isWindows()) { + log.warn("No interpreter found for {}, skipping", step.getData()); + return false; + } + command.add(script.toString()); + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()])); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + + /** + * Returns the interpreter command (and if needed, arguments) to invoke the script file + * + * An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3 + * which will allow the OS to select the correct interpreter for the given file + * + * No special attention is given to "shebang", behavior may differ between OSs + */ + private static ArrayList getInterpreter(Script engine) { + ArrayList interpreter = new ArrayList<>(); + Os osType = SystemUtilities.getOs(); + switch(engine) { + case PS1: + if(osType == Os.WINDOWS) { + interpreter.add("powershell.exe"); + } else if(osType == Os.MAC) { + interpreter.add("/usr/local/bin/pwsh"); + } else { + interpreter.add("pwsh"); + } + interpreter.add("-File"); + break; + case PY: + interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3"); + break; + case BAT: + interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole"); + break; + case RB: + interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby"); + break; + case SH: + default: + // Allow the environment to parse it from the shebang at invocation time + } + return interpreter; + } +} diff --git a/src/qz/installer/provision/invoker/SoftwareInvoker.java b/src/qz/installer/provision/invoker/SoftwareInvoker.java new file mode 100644 index 000000000..bd5c7f843 --- /dev/null +++ b/src/qz/installer/provision/invoker/SoftwareInvoker.java @@ -0,0 +1,87 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Software; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class SoftwareInvoker extends InvokableResource { + private Step step; + + public SoftwareInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File payload = dataToFile(step); + if(payload == null) { + return false; + } + Software installer = Software.parse(step.getData()); + ArrayList command = getInstallCommand(installer, step.getArgs(), payload); + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()])); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + /** + * Returns the installer command (including the installer itself and if needed, arguments) to + * invoke the installer file + */ + public ArrayList getInstallCommand(Software installer, List args, File payload) { + ArrayList interpreter = new ArrayList<>(); + Os os = SystemUtilities.getOs(); + switch(installer) { + case EXE: + if(!SystemUtilities.isWindows()) { + interpreter.add("wine"); + } + // Executable on its own + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume exe args come after payload + break; + case MSI: + interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec"); + interpreter.add("/i"); // Assume standard install + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume msiexec args come after payload + break; + case PKG: + if(os == Os.MAC) { + interpreter.add("installer"); + interpreter.addAll(args); // Assume installer args come before payload + interpreter.add("-package"); + interpreter.add(payload.toString()); + interpreter.add("-target"); + interpreter.add("/"); // Assume we don't want this on a removable volume + } else { + throw new UnsupportedOperationException("PKG is not yet supported on this platform"); + } + break; + case DMG: + // DMG requires "hdiutil attach", but the mount point is unknown + throw new UnsupportedOperationException("DMG is not yet supported"); + case RUN: + if(SystemUtilities.isWindows()) { + interpreter.add("bash"); + interpreter.add("-c"); + } + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume run args come after payload + // Executable on its own + break; + default: + // We'll try to parse it from the shebang just before invocation time + } + return interpreter; + } + +} diff --git a/src/qz/printer/action/html/WebApp.java b/src/qz/printer/action/html/WebApp.java index 1de7df139..b4f1da08f 100644 --- a/src/qz/printer/action/html/WebApp.java +++ b/src/qz/printer/action/html/WebApp.java @@ -185,7 +185,7 @@ 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 - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case WINDOWS: case MAC: System.setProperty("glass.platform", "Monocle"); diff --git a/src/qz/printer/info/NativePrinterMap.java b/src/qz/printer/info/NativePrinterMap.java index b30fb1569..7f07ad55c 100644 --- a/src/qz/printer/info/NativePrinterMap.java +++ b/src/qz/printer/info/NativePrinterMap.java @@ -21,7 +21,7 @@ public abstract class NativePrinterMap extends ConcurrentHashMap(Arrays.asList(args)); + + // Apple grossly allows adding weird flags + // This can be removed when it's removed from unix-launcher.sh.in + if(this.args.size() > 2 && this.args.get(0).startsWith("-NS")) { + this.args.remove(0); + this.args.remove(0); + } } public List getArgs() { return args; @@ -76,8 +84,10 @@ public int getExitCode() { */ private boolean hasFlag(String ... matches) { for(String match : matches) { - if (args.contains(match)) { - return true; + for(String arg : args) { + if(match.equals(arg)) { + return true; + } } } return false; @@ -103,6 +113,22 @@ public boolean hasFlag(ArgValueOption argValueOption) { return hasFlag(argValueOption.getMatches()); } + /** + * Allows a pattern such as "--arg%d" to look for "--arg1", "--arg2" in succession + */ + private String[] valuesOpt(String pattern) throws MissingArgException { + List all = new LinkedList<>(); + int argCounter = 0; + while(true) { + String found = valueOpt(String.format(pattern, ++argCounter)); + if(found == null) { + break; + } + all.add(found); + } + return all.toArray(new String[all.size()]); + } + private String valueOf(String ... matches) throws MissingArgException { return valueOf(false, matches); } @@ -128,6 +154,7 @@ private String valueOf(boolean optional, String ... matches) throws MissingArgEx return val; } } + // FIXME: This doesn't fire when one might expect it to, but fixing it may cause regressions if(!optional) { throw new MissingArgException(); } @@ -227,6 +254,30 @@ public ExitStatus processBuildArgs(ArgValue argValue) { valueOpt("--targetjdk", "-j") ); return SUCCESS; + case PROVISION: + ProvisionBuilder provisionBuilder; + + String jsonParam = valueOpt("--json"); + if(jsonParam != null) { + // Process JSON provision file (overwrites existing provisions) + provisionBuilder = new ProvisionBuilder(new File(jsonParam), valueOpt("--target-os"), valueOpt("--target-arch")); + provisionBuilder.saveJson(true); + } else { + // Process single provision step (preserves existing provisions) + provisionBuilder = new ProvisionBuilder( + valueOf("--type"), + valueOpt("--phase"), + valueOpt("--os"), + valueOpt("--arch"), + valueOf("--data"), + valueOpt("--args"), + valueOpt("--description"), + valuesOpt("--arg%d") + ); + provisionBuilder.saveJson(false); + } + log.info("Successfully added provisioning step(s) {} to file '{}'", provisionBuilder.getJson(), ProvisionBuilder.BUILD_PROVISION_FILE); + return SUCCESS; default: throw new UnsupportedOperationException("Build type " + argValue + " is not yet supported"); } diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index f73b7eb3f..cfe0856d7 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -51,6 +51,8 @@ public enum ArgValue { // Build stubs JLINK(BUILD, "Download, compress and bundle a Java Runtime", "jlink [--platform mac|windows|linux] [--arch x64|aarch64] [--vendor bellsoft|eclipse|...] [--version ...] [--gc hotspot|openj9] [--gcversion ...]", null, "jlink"), + PROVISION(BUILD, "Provision/bundle addition settings or resources into this installer", "provision --json file.json [--target-os windows --target-arch x86_64]", null, + "provision"), // Parameter stubs TRAY_NOTIFICATIONS(PREFERENCES, "Show verbose connect/disconnect notifications in the tray area", null, false, diff --git a/src/qz/utils/ConnectionUtilities.java b/src/qz/utils/ConnectionUtilities.java index 21221ce0c..c5729e3e3 100644 --- a/src/qz/utils/ConnectionUtilities.java +++ b/src/qz/utils/ConnectionUtilities.java @@ -20,7 +20,6 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -191,15 +190,16 @@ public String toString() { } private static String getArch() { - switch(SystemUtilities.getJreArch()) { + switch(SystemUtilities.getArch()) { case X86: case X86_64: return "x86"; case AARCH64: return "arm"; - case RISCV: + case RISCV32: + case RISCV64: return "riscv"; - case PPC: + case PPC64: return "ppc"; default: return "unknown"; @@ -213,13 +213,14 @@ private static String getBitness() { return bitness; } // fallback on some sane, hard-coded values - switch(SystemUtilities.getJreArch()) { - case ARM: + switch(SystemUtilities.getArch()) { + case ARM32: + case RISCV32: case X86: return "32"; case X86_64: - case RISCV: - case PPC: + case RISCV64: + case PPC64: default: return "64"; } @@ -278,7 +279,7 @@ private static String getUserAgentOS() { private static String getUserAgentArch() { String arch; - switch (SystemUtilities.getJreArch()) { + switch (SystemUtilities.getArch()) { case X86: arch = "x86"; break; @@ -289,7 +290,7 @@ private static String getUserAgentArch() { arch = SystemUtilities.OS_ARCH; } - switch(SystemUtilities.getOsType()) { + switch(SystemUtilities.getOs()) { case LINUX: return "Linux " + arch; case WINDOWS: diff --git a/src/qz/utils/FileUtilities.java b/src/qz/utils/FileUtilities.java index 51c54e7ad..52122d3ff 100644 --- a/src/qz/utils/FileUtilities.java +++ b/src/qz/utils/FileUtilities.java @@ -33,6 +33,7 @@ import qz.exception.NullCommandException; import qz.installer.WindowsSpecialFolders; import qz.installer.certificate.CertificateManager; +import qz.installer.provision.ProvisionInstaller; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -47,7 +48,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import static qz.common.Constants.ALLOW_FILE; +import static qz.common.Constants.*; /** * Common static file i/o utilities @@ -893,6 +894,29 @@ public static void setPermissionsRecursively(Path toRecurse, boolean worldWrite) } } + public static void setExecutableRecursively(Path toRecurse, boolean ownerOnly) { + File folder = toRecurse.toFile(); + if(SystemUtilities.isWindows() || !folder.exists() || !folder.isDirectory()) { + return; + } + + // "provision.json" found, assume we're in the provisioning directory, only process scripts and installers + boolean isProvision = toRecurse.resolve(PROVISION_FILE).toFile().exists(); + + try (Stream paths = Files.walk(toRecurse)) { + paths.forEach((path)->{ + if (path.toFile().isDirectory()) { + // Executable bit in Unix allows listing files + path.toFile().setExecutable(true, ownerOnly); + } else if(!isProvision || ProvisionInstaller.shouldBeExecutable(path)) { + path.toFile().setExecutable(true, ownerOnly); + } + }); + } catch (IOException e) { + log.warn("An error occurred setting permissions: {}", toRecurse); + } + } + public static void cleanup() { if(FileUtilities.TEMP_DIR != null) { FileUtils.deleteQuietly(FileUtilities.TEMP_DIR.toFile()); diff --git a/src/qz/utils/LibUtilities.java b/src/qz/utils/LibUtilities.java index a6790dc5c..1fd172584 100644 --- a/src/qz/utils/LibUtilities.java +++ b/src/qz/utils/LibUtilities.java @@ -3,6 +3,7 @@ import com.sun.jna.Platform; import javafx.application.Application; import org.usb4java.Loader; +import qz.build.provision.params.Os; import qz.common.Constants; import java.io.IOException; @@ -36,12 +37,12 @@ public class LibUtilities { private final Path basePath; // Common native file extensions, by platform - private static HashMap extensionMap = new HashMap<>(); + 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" }); + extensionMap.put(Os.WINDOWS, new String[]{ "dll" }); + extensionMap.put(Os.MAC, new String[]{ "jnilib", "dylib" }); + extensionMap.put(Os.LINUX, new String[]{ "so" }); + extensionMap.put(Os.UNKNOWN, new String[]{ "so" }); } public LibUtilities() { @@ -78,7 +79,7 @@ public void bind() { * 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[] extensions = extensionMap.get(SystemUtilities.getOs()); String prefix = !SystemUtilities.isWindows() ? "lib" : ""; List found = new ArrayList<>(); try (Stream walkStream = Files.walk(basePath)) { @@ -170,7 +171,7 @@ 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()) { + switch(SystemUtilities.getOs()) { case WINDOWS: return !graphicsJar.contains("windows"); case MAC: diff --git a/src/qz/utils/ShellUtilities.java b/src/qz/utils/ShellUtilities.java index 6f8b61dea..0c90938de 100644 --- a/src/qz/utils/ShellUtilities.java +++ b/src/qz/utils/ShellUtilities.java @@ -18,10 +18,7 @@ import java.io.*; import java.lang.reflect.Method; import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +import java.util.*; /** * Utility class for managing all {@code Runtime.exec(...)} functions. @@ -32,26 +29,25 @@ public class ShellUtilities { private static final Logger log = LogManager.getLogger(ShellUtilities.class); + // Display envp values in errors and console logs + private static boolean debugEnvp = false; + // Shell environment overrides. null = don't override - public static String[] envp = null; + private static Map env = new HashMap<>(System.getenv()); + @Deprecated /* TODO: Make private, use getEnvp() instead */ + public static String[] envp = getEnvp(); // Make sure all shell calls are LANG=en_US.UTF-8 static { - if (!SystemUtilities.isWindows()) { - // Cache existing; permit named overrides w/o full clobber - Map env = new HashMap<>(System.getenv()); - if (SystemUtilities.isMac()) { + switch(SystemUtilities.getOs()) { + case WINDOWS: + break; + case MAC: // Enable LANG overrides - env.put("SOFTWARE", ""); - } - // Functional equivalent of "export LANG=en_US.UTF-8" - env.put("LANG", "C"); - String[] envp = new String[env.size()]; - int i = 0; - for(Map.Entry o : env.entrySet()) - envp[i++] = o.getKey() + "=" + o.getValue(); - - ShellUtilities.envp = envp; + addEnvp("SOFTWARE", ""); + default: + // Functional equivalent of "export LANG=en_US.UTF-8" + addEnvp("LANG", "C"); } } @@ -94,14 +90,15 @@ public static boolean execute(String[] commandArray, boolean silent) { Process p = Runtime.getRuntime().exec(commandArray, envp); // Consume output to prevent deadlock while (p.getInputStream().read() != -1) {} + while (p.getErrorStream().read() != -1) {} p.waitFor(); return p.exitValue() == 0; } catch(InterruptedException ex) { - log.warn("InterruptedException waiting for a return value: {} envp: {}", Arrays.toString(commandArray), Arrays.toString(envp), ex); + log.warn("InterruptedException waiting for a return value: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); } catch(IOException ex) { - log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), Arrays.toString(envp), ex); + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); } return false; @@ -156,7 +153,7 @@ public static String execute(String[] commandArray, String[] searchFor, boolean } } catch(IOException ex) { - log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), Arrays.toString(envp), ex); + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); } finally { if (stdInput != null) { @@ -199,7 +196,7 @@ public static String executeRaw(String[] commandArray, boolean silent) { } catch(IOException ex) { if(!silent) { - log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), Arrays.toString(envp), ex); + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); } } finally { @@ -277,4 +274,40 @@ public static void browseDirectory(File directory) { } } } + + /** + * Provides fast envp manipulation for starting processes with additional environmental variables + * + * @param paired Pairs of values, e.g. { "FOO", "BAR" } where the environment will set FOO=BAR + * @return + */ + public static synchronized String[] addEnvp(Object ... paired) { + if(paired.length % 2 != 0) { + throw new UnsupportedOperationException("Values must be provided in pairs"); + } + + for(int i = 0; i < paired.length / 2; i++) { + env.put(paired[2 * i].toString(), paired[2 * i + 1].toString()); + } + envp = null; + return getEnvp(); + } + + public static synchronized String[] getEnvp() { + if(envp == null) { + String[] temp = new String[env.size()]; + int i = 0; + for(Map.Entry o : env.entrySet()) + temp[i++] = o.getKey() + "=" + o.getValue(); + envp = temp; + } + return envp; + } + + public static String envpToString() { + if(debugEnvp) { + return Arrays.toString(envp); + } + return "(suppressed)"; + } } diff --git a/src/qz/utils/SystemUtilities.java b/src/qz/utils/SystemUtilities.java index 979e5c275..d821ad5f6 100644 --- a/src/qz/utils/SystemUtilities.java +++ b/src/qz/utils/SystemUtilities.java @@ -18,6 +18,8 @@ import org.joor.ReflectException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import qz.build.provision.params.Arch; +import qz.build.provision.params.Os; import qz.common.Constants; import qz.common.TrayManager; import qz.installer.Installer; @@ -50,8 +52,8 @@ public class SystemUtilities { 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 Os OS_TYPE = Os.bestMatch(OS_NAME); + private static final Arch JRE_ARCH = Arch.bestMatch(OS_ARCH); private static final Logger log = LogManager.getLogger(TrayManager.class); private static final Locale defaultLocale = Locale.getDefault(); @@ -74,73 +76,14 @@ public class SystemUtilities { 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() { + public static Os getOs() { 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() { + public static Arch getArch() { return JRE_ARCH; } - 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; - } - /** * Call to workaround Locale-specific bugs (See issue #680) * Please call restoreLocale() as soon as possible @@ -393,7 +336,7 @@ public static Path getAppPath() { * @return {@code true} if Windows, {@code false} otherwise */ public static boolean isWindows() { - return OS_TYPE == OsType.WINDOWS; + return OS_TYPE == Os.WINDOWS; } /** @@ -402,7 +345,7 @@ public static boolean isWindows() { * @return {@code true} if Mac OS, {@code false} otherwise */ public static boolean isMac() { - return OS_TYPE == OsType.MAC; + return OS_TYPE == Os.MAC; } /** @@ -411,7 +354,7 @@ public static boolean isMac() { * @return {@code true} if Linux, {@code false} otherwise */ public static boolean isLinux() { - return OS_TYPE == OsType.LINUX; + return OS_TYPE == Os.LINUX; } /** @@ -422,7 +365,7 @@ public static boolean isLinux() { public static boolean isUnix() { if(OS_NAME != null) { String osLower = OS_NAME.toLowerCase(Locale.ENGLISH); - return OS_TYPE == OsType.MAC || OS_TYPE == OsType.SOLARIS || OS_TYPE == OsType.LINUX || + return OS_TYPE == Os.MAC || OS_TYPE == Os.SOLARIS || OS_TYPE == Os.LINUX || osLower.contains("nix") || osLower.indexOf("aix") > 0; } return false; @@ -434,7 +377,7 @@ public static boolean isUnix() { * @return {@code true} if Solaris, {@code false} otherwise */ public static boolean isSolaris() { - return OS_TYPE == OsType.SOLARIS; + return OS_TYPE == Os.SOLARIS; } public static boolean isDarkTaskbar() { @@ -829,7 +772,7 @@ public static boolean validateSaltedChallenge(String message) { */ public static boolean isSystemTraySupported(boolean headless) { if(!headless) { - switch(getOsType()) { + switch(getOs()) { case WINDOWS: if(WindowsUtilities.isHiddenSystemTray()) { return false; diff --git a/src/qz/utils/WindowsUtilities.java b/src/qz/utils/WindowsUtilities.java index a6c15fa4f..714fe3042 100644 --- a/src/qz/utils/WindowsUtilities.java +++ b/src/qz/utils/WindowsUtilities.java @@ -15,6 +15,7 @@ import com.sun.jna.ptr.IntByReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import qz.build.provision.params.Arch; import qz.common.Constants; import java.awt.*; @@ -449,7 +450,7 @@ public static boolean isWow64() { if(isWow64 == null) { isWow64 = false; if (SystemUtilities.isWindows()) { - if (SystemUtilities.getJreArch() != JreArch.X86_64) { + if (SystemUtilities.getArch() != Arch.X86_64) { isWow64 = System.getenv("PROGRAMFILES(x86)") != null; } } diff --git a/test/qz/installer/provision/ProvisionerInstallerTests.java b/test/qz/installer/provision/ProvisionerInstallerTests.java new file mode 100644 index 000000000..69b15312d --- /dev/null +++ b/test/qz/installer/provision/ProvisionerInstallerTests.java @@ -0,0 +1,20 @@ +package qz.installer.provision; + +import org.codehaus.jettison.json.JSONException; +import qz.common.Constants; + +import java.io.IOException; +import java.io.InputStream; + +public class ProvisionerInstallerTests { + + public static void main(String ... args) throws JSONException, IOException { + InputStream in = ProvisionerInstallerTests.class.getResourceAsStream("resources/" + Constants.PROVISION_FILE); + + // Parse the JSON + ProvisionInstaller provisionInstaller = new ProvisionInstaller(ProvisionerInstallerTests.class, in); + + // Invoke all parsed steps + provisionInstaller.invoke(); + } +} diff --git a/test/qz/installer/provision/resources/cert1.crt b/test/qz/installer/provision/resources/cert1.crt new file mode 100644 index 000000000..4ec319a04 --- /dev/null +++ b/test/qz/installer/provision/resources/cert1.crt @@ -0,0 +1,60 @@ +-----BEGIN CERTIFICATE----- +MIIFAzCCAuugAwIBAgICEAIwDQYJKoZIhvcNAQEFBQAwgZgxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEbMBkGA1UECgwSUVogSW5kdXN0cmllcywgTExDMRswGQYD +VQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxGTAXBgNVBAMMEHF6aW5kdXN0cmllcy5j +b20xJzAlBgkqhkiG9w0BCQEWGHN1cHBvcnRAcXppbmR1c3RyaWVzLmNvbTAeFw0x +NTAzMTkwMjM4NDVaFw0yNTAzMTkwMjM4NDVaMHMxCzAJBgNVBAYTAkFBMRMwEQYD +VQQIDApTb21lIFN0YXRlMQ0wCwYDVQQKDAREZW1vMQ0wCwYDVQQLDAREZW1vMRIw +EAYDVQQDDAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtFzbBDRTDHHmlSVQLqjY +aoGax7ql3XgRGdhZlNEJPZDs5482ty34J4sI2ZK2yC8YkZ/x+WCSveUgDQIVJ8oK +D4jtAPxqHnfSr9RAbvB1GQoiYLxhfxEp/+zfB9dBKDTRZR2nJm/mMsavY2DnSzLp +t7PJOjt3BdtISRtGMRsWmRHRfy882msBxsYug22odnT1OdaJQ54bWJT5iJnceBV2 +1oOqWSg5hU1MupZRxxHbzI61EpTLlxXJQ7YNSwwiDzjaxGrufxc4eZnzGQ1A8h1u +jTaG84S1MWvG7BfcPLW+sya+PkrQWMOCIgXrQnAsUgqQrgxQ8Ocq3G4X9UvBy5VR +CwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdl +bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUpG420UhvfwAFMr+8vf3pJunQ +gH4wHwYDVR0jBBgwFoAUkKZQt4TUuepf8gWEE3hF6Kl1VFwwDQYJKoZIhvcNAQEF +BQADggIBAFXr6G1g7yYVHg6uGfh1nK2jhpKBAOA+OtZQLNHYlBgoAuRRNWdE9/v4 +J/3Jeid2DAyihm2j92qsQJXkyxBgdTLG+ncILlRElXvG7IrOh3tq/TttdzLcMjaR +8w/AkVDLNL0z35shNXih2F9JlbNRGqbVhC7qZl+V1BITfx6mGc4ayke7C9Hm57X0 +ak/NerAC/QXNs/bF17b+zsUt2ja5NVS8dDSC4JAkM1dD64Y26leYbPybB+FgOxFu +wou9gFxzwbdGLCGboi0lNLjEysHJBi90KjPUETbzMmoilHNJXw7egIo8yS5eq8RH +i2lS0GsQjYFMvplNVMATDXUPm9MKpCbZ7IlJ5eekhWqvErddcHbzCuUBkDZ7wX/j +unk/3DyXdTsSGuZk3/fLEsc4/YTujpAjVXiA1LCooQJ7SmNOpUa66TPz9O7Ufkng ++CoTSACmnlHdP7U9WLr5TYnmL9eoHwtb0hwENe1oFC5zClJoSX/7DRexSJfB7YBf +vn6JA2xy4C6PqximyCPisErNp85GUcZfo33Np1aywFv9H+a83rSUcV6kpE/jAZio +5qLpgIOisArj1HTM6goDWzKhLiR/AeG3IJvgbpr9Gr7uZmfFyQzUjvkJ9cybZRd+ +G8azmpBBotmKsbtbAU/I/LVk8saeXznshOVVpDRYtVnjZeAneso7 +-----END CERTIFICATE----- +--START INTERMEDIATE CERT-- +-----BEGIN CERTIFICATE----- +MIIFEjCCA/qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgawxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTESMBAGA1UEBwwJQ2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJ +bmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsMElFaIEluZHVzdHJpZXMsIExMQzEZMBcG +A1UEAwwQcXppbmR1c3RyaWVzLmNvbTEnMCUGCSqGSIb3DQEJARYYc3VwcG9ydEBx +emluZHVzdHJpZXMuY29tMB4XDTE1MDMwMjAwNTAxOFoXDTM1MDMwMjAwNTAxOFow +gZgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEbMBkGA1UECgwSUVogSW5kdXN0 +cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxGTAXBgNVBAMM +EHF6aW5kdXN0cmllcy5jb20xJzAlBgkqhkiG9w0BCQEWGHN1cHBvcnRAcXppbmR1 +c3RyaWVzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTDgNLU +iohl/rQoZ2bTMHVEk1mA020LYhgfWjO0+GsLlbg5SvWVFWkv4ZgffuVRXLHrwz1H +YpMyo+Zh8ksJF9ssJWCwQGO5ciM6dmoryyB0VZHGY1blewdMuxieXP7Kr6XD3GRM +GAhEwTxjUzI3ksuRunX4IcnRXKYkg5pjs4nLEhXtIZWDLiXPUsyUAEq1U1qdL1AH +EtdK/L3zLATnhPB6ZiM+HzNG4aAPynSA38fpeeZ4R0tINMpFThwNgGUsxYKsP9kh +0gxGl8YHL6ZzC7BC8FXIB/0Wteng0+XLAVto56Pyxt7BdxtNVuVNNXgkCi9tMqVX +xOk3oIvODDt0UoQUZ/umUuoMuOLekYUpZVk4utCqXXlB4mVfS5/zWB6nVxFX8Io1 +9FOiDLTwZVtBmzmeikzb6o1QLp9F2TAvlf8+DIGDOo0DpPQUtOUyLPCh5hBaDGFE +ZhE56qPCBiQIc4T2klWX/80C5NZnd/tJNxjyUyk7bjdDzhzT10CGRAsqxAnsjvMD +2KcMf3oXN4PNgyfpbfq2ipxJ1u777Gpbzyf0xoKwH9FYigmqfRH2N2pEdiYawKrX +6pyXzGM4cvQ5X1Yxf2x/+xdTLdVaLnZgwrdqwFYmDejGAldXlYDl3jbBHVM1v+uY +5ItGTjk+3vLrxmvGy5XFVG+8fF/xaVfo5TW5AgMBAAGjUDBOMB0GA1UdDgQWBBSQ +plC3hNS56l/yBYQTeEXoqXVUXDAfBgNVHSMEGDAWgBQDRcZNwPqOqQvagw9BpW0S +BkOpXjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAJIO8SiNr9jpLQ +eUsFUmbueoxyI5L+P5eV92ceVOJ2tAlBA13vzF1NWlpSlrMmQcVUE/K4D01qtr0k +gDs6LUHvj2XXLpyEogitbBgipkQpwCTJVfC9bWYBwEotC7Y8mVjjEV7uXAT71GKT +x8XlB9maf+BTZGgyoulA5pTYJ++7s/xX9gzSWCa+eXGcjguBtYYXaAjjAqFGRAvu +pz1yrDWcA6H94HeErJKUXBakS0Jm/V33JDuVXY+aZ8EQi2kV82aZbNdXll/R6iGw +2ur4rDErnHsiphBgZB71C5FD4cdfSONTsYxmPmyUb5T+KLUouxZ9B0Wh28ucc1Lp +rbO7BnjW +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/qz/installer/provision/resources/provision.json b/test/qz/installer/provision/resources/provision.json new file mode 100644 index 000000000..f596df548 --- /dev/null +++ b/test/qz/installer/provision/resources/provision.json @@ -0,0 +1,117 @@ + +[ + { + "description": "[ERROR EXPECTED] missing 'type' and 'phase'", + "os": "*", + "data": "foo.bar=1" + }, + { + "description": "[ERROR EXPECTED] invalid 'remover' id", + "type": "remover", + "os": "*", + "phase": "install", + "data": "bbb" + }, + { + "description": "[ERROR EXPECTED] missing 'type'", + "os": "*", + "phase": "install", + "data": "this_file_does_not_exist" + }, + { + "description": "[ERROR EXPECTED] 'data' file missing", + "type": "script", + "os": "*", + "phase": "install", + "data": "this_file_does_not_exist" + }, + { + "description": "[ERROR EXPECTED] 'arch' is invalid", + "type": "property", + "os": "*", + "arch": "sparc", + "data": "bar.foo=2" + }, + { + "description": "[ERROR EXPECTED] 'os' is invalid", + "type": "property", + "os": "quake", + "arch": "*", + "data": "bar.foo=2" + }, + { + "description": "[WINDOWS SCRIPT] powershell at 'install'", + "type": "script", + "os": "windows", + "phase": "install", + "data": "script1.ps1" + }, + { + "description": "[MAC SCRIPT] powershell at 'install'", + "type": "script", + "os": "mac", + "phase": "install", + "data": "script1.ps1" + }, + { + "description": "[LINUX SCRIPT] python at 'startup'", + "type": "script", + "os": "linux", + "phase": "startup", + "data": "script4.py" + }, + { + "description": "[LINUX & MAC SCRIPT] bash without extension at 'install'", + "type": "script", + "os": "linux|mac", + "phase": "install", + "data": "script2" + }, + { + "description": "[ALL OS SCRIPT] with '.sh' extension at 'install'", + "type": "script", + "os": "*", + "phase": "install", + "data": "script3.sh" + }, + { + "description": "[AARCH64 ONLY SCRIPT] with '.sh' extension at 'install'", + "type": "script", + "os": "*", + "arch": "aarch64", + "phase": "install", + "data": "script2" + }, + { + "description": "[CERTIFICATE] at 'startup' (allowed.dat)", + "type": "cert", + "os": "*", + "data": "cert1.crt" + }, + { + "description": "[PROPERTY] at 'certgen' (qz-tray.properties)", + "type": "property", + "os": "*", + "data": "log.size=2097152" + }, + { + "description": "[PREFERENCE] at 'startup' (prefs.properties)", + "type": "preference", + "os": "*", + "data": "tray.notifications=true" + }, + { + "description": "[REMOVER] at 'install' ('QZ Tray' rebranded 'Cherry Connect')", + "type": "remover", + "os": "*", + "phase": "install", + "data": "Cherry Connect,cc-util,cc" + }, + { + "description": "[REMOVER] at 'install' QZ-branded version", + "type": "remover", + "os": "*", + "phase": "install", + "data": "qz" + } +] \ No newline at end of file diff --git a/test/qz/installer/provision/resources/script1.ps1 b/test/qz/installer/provision/resources/script1.ps1 new file mode 100644 index 000000000..3a7c68e62 --- /dev/null +++ b/test/qz/installer/provision/resources/script1.ps1 @@ -0,0 +1,5 @@ +$shell="PowerShell" +$date="$(Get-Date -format "yyyy-MM-dd HH:mm:ss")" +$script="$($myInvocation.MyCommand.Name)" +# FIXME: ~/Desktop may try to write to /root/Desktop on Linux +echo "$date Successful provisioning test from '$shell': $script" >> ~/Desktop/provision.log \ No newline at end of file diff --git a/test/qz/installer/provision/resources/script2 b/test/qz/installer/provision/resources/script2 new file mode 100644 index 000000000..81165cb6e --- /dev/null +++ b/test/qz/installer/provision/resources/script2 @@ -0,0 +1,9 @@ +#!/bin/bash + +shell=$(ps -p $$ -oargs=|awk '{print $1}') +date=$(date "+%F %T") +script=$(basename "$0") +user="$(eval echo ~$(logname))" + +echo "$date Successful provisioning test from '$shell': $script" >> "$user/Desktop/provision.log" +chmod 555 "$user/Desktop/provision.log" \ No newline at end of file diff --git a/test/qz/installer/provision/resources/script3.sh b/test/qz/installer/provision/resources/script3.sh new file mode 100644 index 000000000..f05f99a60 --- /dev/null +++ b/test/qz/installer/provision/resources/script3.sh @@ -0,0 +1,7 @@ +shell=$(ps -p $$ -oargs=|awk '{print $1}') +date=$(date "+%F %T") +script=$(basename "$0") +user="$(eval echo ~$(logname))" + +echo "$date Successful provisioning test from '$shell': $script" >> "$user/Desktop/provision.log" +chmod 555 "$user/Desktop/provision.log" \ No newline at end of file diff --git a/test/qz/installer/provision/resources/script4.py b/test/qz/installer/provision/resources/script4.py new file mode 100644 index 000000000..e34d47bd9 --- /dev/null +++ b/test/qz/installer/provision/resources/script4.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import os + +def notify(title, message): + os.system(f"notify-send '{title}' '{message}'") + +title=os.getenv('APP_TITLE') +version=os.getenv('APP_VERSION') +printer="\U0001F5A8" +tada="\U0001F389" + +notify("{} {}".format(printer, title), """{} This is a sample message from {} {}. + +This message indicates that provisioning startup tasks are working.""".format(tada, title, version)) \ No newline at end of file From 0c002b7fb8fbd94ae33c674fd0d0b6d27b230e09 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Sat, 25 Nov 2023 14:21:44 -0500 Subject: [PATCH 2/2] Fix missing import --- src/qz/utils/ConnectionUtilities.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qz/utils/ConnectionUtilities.java b/src/qz/utils/ConnectionUtilities.java index c5729e3e3..466a02828 100644 --- a/src/qz/utils/ConnectionUtilities.java +++ b/src/qz/utils/ConnectionUtilities.java @@ -20,6 +20,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.StringUtils;