From 5b5f342adf51d2b904446b395d0f9e85d8e8e485 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Tue, 17 Oct 2023 02:00:21 -0400 Subject: [PATCH] Initial working version --- build.xml | 1 + src/qz/App.java | 4 +- src/qz/build/provision/Provisionee.java | 146 +++++++++++++----- src/qz/installer/Installer.java | 22 +-- src/qz/installer/provision/Provisioner.java | 6 +- src/qz/installer/provision/Step.java | 80 +++++++--- src/qz/utils/ArgParser.java | 32 ++-- .../provision/ProvisionerTests.java | 3 +- .../provision/resources/cert1.crt | 0 .../provision/resources/provision.json | 4 +- .../provision/resources/script1.ps1 | 0 .../provision/resources/script2 | 0 .../provision/resources/script3.sh | 0 13 files changed, 210 insertions(+), 88 deletions(-) rename test/qz/{build => installer}/provision/ProvisionerTests.java (87%) rename test/qz/{build => installer}/provision/resources/cert1.crt (100%) rename test/qz/{build => installer}/provision/resources/provision.json (95%) rename test/qz/{build => installer}/provision/resources/script1.ps1 (100%) rename test/qz/{build => installer}/provision/resources/script2 (100%) rename test/qz/{build => installer}/provision/resources/script3.sh (100%) diff --git a/build.xml b/build.xml index fb06c1487..ada9555f3 100644 --- a/build.xml +++ b/build.xml @@ -44,6 +44,7 @@ + diff --git a/src/qz/App.java b/src/qz/App.java index a1d11371c..3cfb5ee2e 100644 --- a/src/qz/App.java +++ b/src/qz/App.java @@ -71,10 +71,10 @@ public static void main(String ... args) { // Invoke any provisioning steps that are phase=startup try { - Provisioner provisioner = new Provisioner(); + Provisioner provisioner = new Provisioner(SystemUtilities.getJarParentPath().resolve("provision")); provisioner.invoke(Step.Phase.STARTUP); } catch(Exception e) { - log.warn("An error occurred provisioning \"phase\": \"startup\" entries"); + log.warn("An error occurred provisioning \"phase\": \"startup\" entries", e); } try { diff --git a/src/qz/build/provision/Provisionee.java b/src/qz/build/provision/Provisionee.java index 22331e287..cf69b5812 100644 --- a/src/qz/build/provision/Provisionee.java +++ b/src/qz/build/provision/Provisionee.java @@ -1,19 +1,26 @@ package qz.build.provision; import org.apache.commons.io.FileUtils; +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.installer.provision.Step; +import qz.utils.SystemUtilities; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; - -import static qz.installer.Installer.*; +import java.nio.file.Path; public class Provisionee { - private JSONObject jsonStep; + protected static final Logger log = LogManager.getLogger(Provisionee.class); + + public static final Path BUILD_PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve("provision"); + public static final File BUILD_PROVISION_FILE = BUILD_PROVISION_FOLDER.resolve("provision.json").toFile(); + + private JSONArray jsonSteps; /** * Parses input to create a Provisionee object, a JSON formatted setting or bundled resource useful for @@ -28,59 +35,111 @@ public class Provisionee { * @param varArgs SCRIPT or INSTALL only, will honor spaces when invoked */ public Provisionee(String type, String phase, String os, String data, String args, String description, String ... varArgs) throws IOException, JSONException { - if(!PROVISION_FOLDER.toFile().isDirectory() && !PROVISION_FOLDER.toFile().mkdirs()) { - throw new IOException("Could not create provision destination:" + PROVISION_FOLDER); - } + createProvisionDirectory(false); + jsonSteps = new JSONArray(); // Wrap into JSON so that we can save it - jsonStep = new JSONObject(); - put("description", description); - put("type", type); - put("phase", phase); - put("os", os); - put("data", data); - put("args", args); - put("arg%d", varArgs); - - // Step will perform basic parsing/sanity checks - Step step = Step.parse(jsonStep); + JSONObject jsonStep = new JSONObject(); + put(jsonStep, "description", description); + put(jsonStep, "type", type); + put(jsonStep, "phase", phase); + put(jsonStep, "os", os); + put(jsonStep, "data", data); + put(jsonStep, "args", args); + put(jsonStep, "arg%d", varArgs); + + processStep(jsonStep); + } + + public Provisionee(File jsonFile) throws IOException, JSONException { + createProvisionDirectory(true); + jsonSteps = new JSONArray(); - // Copy resource to provision folder - if(step.getType() == Step.Type.INSTALLER || step.getType() == Step.Type.SCRIPT) { - File src = new File(data); - File dest = PROVISION_FOLDER.resolve(src.getName()).toFile(); - FileUtils.copyFile(src, dest); - if(dest.exists()) { - jsonStep.remove("data"); - jsonStep.put("data", PROVISION_FOLDER.relativize(dest.toPath())); + String jsonData = FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8); + JSONArray pendingSteps = new JSONArray(jsonData); + + // Cycle through so that each Step can be individually processed + for(int i = 0; i < pendingSteps.length(); i++) { + JSONObject jsonStep = pendingSteps.getJSONObject(i); + try { + processStep(jsonStep); + } catch(Exception e) { + log.warn("Skipping step {}", jsonStep, e); } } + } - public JSONObject getJson() { - return jsonStep; + public JSONArray getJson() { + return jsonSteps; + } + + /** + * Construct as a Step to perform basic parsing/sanity checks + * Copy resources (if needed) to provisioning directory + */ + private void processStep(JSONObject jsonStep) throws JSONException, IOException { + Step step = Step.parse(jsonStep); + if(saveResources(jsonStep, step)) { + jsonSteps.put(jsonStep); + } else { + log.warn("Skipping step. Resources could not be saved {}", step); + } + } + + /** + * Save any resources files required for INSTALL and SCRIPT steps to provision folder + */ + public boolean saveResources(JSONObject jsonStep, Step step) throws IOException, JSONException { + switch(step.getType()) { + case CERT: + case SCRIPT: + case INSTALLER: + File src = new File(step.getData()); + String fileName = src.getName(); + if(fileName.equals(BUILD_PROVISION_FILE.getName())) { + throw new IOException("Skipping step. Resource name conflicts with provision file " + fileName); + } + File dest = BUILD_PROVISION_FOLDER.resolve(fileName).toFile(); + FileUtils.copyFile(src, dest); + if(dest.exists()) { + jsonStep.remove("data"); + jsonStep.put("data", BUILD_PROVISION_FOLDER.relativize(dest.toPath())); + } else { + return false; + } + break; + default: + } + return true; } /** * Appends the JSONObject to the end of the provisionFile */ - public boolean saveJson() throws IOException, JSONException { - JSONArray jsonSteps; - if(PROVISION_FILE.exists()) { - String jsonData = FileUtils.readFileToString(PROVISION_FILE, StandardCharsets.UTF_8); - jsonSteps = new JSONArray(jsonData); + 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 { - jsonSteps = new JSONArray(); + mergeSteps = new JSONArray(); } - jsonSteps.put(jsonStep); - FileUtils.writeStringToFile(PROVISION_FILE, jsonSteps.toString(3), StandardCharsets.UTF_8); + + // 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 void put(String name, String val) throws JSONException { + private static void put(JSONObject jsonStep, String name, String val) throws JSONException { if(val != null && !val.isEmpty()) { jsonStep.put(name, val); } @@ -90,10 +149,23 @@ private void put(String name, String val) throws JSONException { * Convenience method for adding consecutive patterned value pairs into the JSONObject * e.g. --arg1 "foo" --arg2 "bar" */ - private void put(String pattern, String ... varArgs) throws JSONException { + private static void put(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 new IOException("Could not create provision destination:" + BUILD_PROVISION_FOLDER); + } } diff --git a/src/qz/installer/Installer.java b/src/qz/installer/Installer.java index c8d084868..31c3c221d 100644 --- a/src/qz/installer/Installer.java +++ b/src/qz/installer/Installer.java @@ -42,9 +42,6 @@ public abstract class Installer { public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent")); public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime"; - public static final Path PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve("provision"); - public static final File PROVISION_FILE = PROVISION_FOLDER.resolve("provision.json").toFile(); - public enum PrivilegeLevel { USER, SYSTEM @@ -106,7 +103,7 @@ public static void install() throws Exception { .addAppLauncher() .addStartupEntry() .addSystemSettings() - .addProvisioning(); + .addProvisioning(Step.Phase.INSTALL); } public static void uninstall() { @@ -285,6 +282,9 @@ 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 + addProvisioning(Step.Phase.CERTGEN); + return certificateManager; } @@ -322,22 +322,24 @@ public Installer addUserSettings() { return instance; } - public Installer addProvisioning() { + public Installer addProvisioning(Step.Phase phase) { try { - Provisioner provisioner = new Provisioner(); - provisioner.invoke(Step.Phase.INSTALL); + Path provisionPath = Paths.get(getDestination()).resolve("provision"); + Provisioner provisioner = new Provisioner(provisionPath); + provisioner.invoke(phase); } catch(Exception e) { - log.warn("An error occurred provisioning \"phase\": \"install\" entries"); + log.warn("An error occurred provisioning \"phase\": \"install\" entries", e); } return this; } public Installer removeProvisioning() { try { - Provisioner provisioner = new Provisioner(); + Path provisionPath = Paths.get(getDestination()).resolve("provision"); + Provisioner provisioner = new Provisioner(provisionPath); provisioner.invoke(Step.Phase.UNINSTALL); } catch(Exception e) { - log.warn("An error occurred provisioning \"phase\": \"uninstall\" entries"); + log.warn("An error occurred provisioning \"phase\": \"uninstall\" entries", e); } return this; } diff --git a/src/qz/installer/provision/Provisioner.java b/src/qz/installer/provision/Provisioner.java index f18250ce6..3b1d78c98 100644 --- a/src/qz/installer/provision/Provisioner.java +++ b/src/qz/installer/provision/Provisioner.java @@ -7,21 +7,21 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; -import qz.installer.Installer; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; public class Provisioner { protected static final Logger log = LogManager.getLogger(Provisioner.class); private ArrayList steps; - public Provisioner() throws IOException, JSONException { - this(Installer.PROVISION_FOLDER, Installer.PROVISION_FILE); + public Provisioner(Path provisionFolder) throws IOException, JSONException { + this(provisionFolder, provisionFolder.resolve("provision.json").toFile()); } public Provisioner(Class relativeClass, InputStream in) throws IOException, JSONException { diff --git a/src/qz/installer/provision/Step.java b/src/qz/installer/provision/Step.java index dd2123632..7e84b6d42 100644 --- a/src/qz/installer/provision/Step.java +++ b/src/qz/installer/provision/Step.java @@ -85,6 +85,7 @@ public static List parse(String input) { public enum Phase { INSTALL, + CERTGEN, STARTUP, UNINSTALL; @@ -239,8 +240,8 @@ public ArrayList getInstallCommand(List args, File payload) { private Phase phase; private String data; - private Class relativeClass; private Path relativePath; + private Class relativeClass; public Step(Type type, List os, Phase phase, String data, List args) { this.type = type; @@ -256,7 +257,16 @@ public Step(Type type, List os, Phase phase, String data, List args) // Handle nulls if(phase == null) { - this.phase = type == Type.PREFERENCE ? Phase.STARTUP : Phase.INSTALL; + switch(this.type) { + case PREFERENCE: + this.phase = Phase.STARTUP; + break; + case PROPERTY: + this.phase = Phase.CERTGEN; + break; + default: + this.phase = Phase.INSTALL; + } log.info("Phase is null, defaulting to {} based on Type {}", this.phase, this.type); } if(os.size() == 0) { @@ -265,14 +275,26 @@ public Step(Type type, List os, Phase phase, String data, List args) } // Override conflicts - if (type == Type.PROPERTY || type == Type.CERT) { - if (phase == Phase.STARTUP) { + if (type == Type.INSTALLER) { + if (phase != Phase.INSTALL) { this.phase = Phase.INSTALL; log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase); } } if (type == Type.PREFERENCE) { - if (phase == Phase.INSTALL) { + if (phase != Phase.STARTUP) { + this.phase = Phase.STARTUP; + log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase); + } + } + if (type == Type.PROPERTY) { + if (phase != Phase.CERTGEN) { + this.phase = Phase.CERTGEN; + log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase); + } + } + if (type == Type.CERT) { + if (phase != Phase.STARTUP) { this.phase = Phase.STARTUP; log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase); } @@ -306,8 +328,17 @@ public boolean invoke() throws Exception { PropertyHelper prefs = new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties"); return provisionProperties(prefs); case PROPERTY: - // TODO: How should we pass this parameter in? - PropertyHelper props = new PropertyHelper(SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile()); + File propertiesFile; + if(relativePath != null) { + // Assume qz-tray.properties is one directory up from provision folder + // required to prevent installing to payload + propertiesFile = relativePath.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: {}", propertiesFile); + PropertyHelper props = new PropertyHelper(propertiesFile); return provisionProperties(props); default: throw new UnsupportedOperationException("Type " + type + " is not yet supported."); @@ -419,21 +450,28 @@ private AbstractMap.SimpleEntry parsePropertyPair(String prop) { } private File resourceToFile() throws IOException { - InputStream in = relativeClass.getResourceAsStream(this.data); - if(in == null) { - log.warn("Resource '{}' is missing, skipping step", this.data); - return null; - } - String suffix = "_" + Paths.get(this.data).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(this.type == Type.SCRIPT && !SystemUtilities.isWindows()) { - destination.setExecutable(true, true); + if(relativeClass != null) { + // Resource may be inside the jar + InputStream in = relativeClass.getResourceAsStream(this.data); + if(in == null) { + log.warn("Resource '{}' is missing, skipping step", this.data); + return null; + } + String suffix = "_" + Paths.get(this.data).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(this.type == Type.SCRIPT && !SystemUtilities.isWindows()) { + destination.setExecutable(true, true); + } + return destination; + } else if(relativePath != null) { + // Resource is in a physical folder + return relativePath.resolve(this.data).toFile(); } - return destination; + return null; } public static Step parse(JSONObject jsonStep) { diff --git a/src/qz/utils/ArgParser.java b/src/qz/utils/ArgParser.java index dd82b031a..374b1e742 100644 --- a/src/qz/utils/ArgParser.java +++ b/src/qz/utils/ArgParser.java @@ -250,17 +250,27 @@ public ExitStatus processBuildArgs(ArgValue argValue) { ); return SUCCESS; case PROVISION: - Provisionee provisionee = new Provisionee( - valueOf("--type"), - valueOpt("--phase"), - valueOpt("--os"), - valueOf("--data"), - valueOpt("--args"), - valueOpt("--description"), - valuesOpt("--arg%d") - ); - provisionee.saveJson(); - log.info("Successfully added provisioning step {} to file '{}'", provisionee.getJson(), Installer.PROVISION_FILE); + Provisionee provisionee; + + String jsonParam = valueOpt("--json"); + if(jsonParam != null) { + // Process JSON provision file (overwrites existing provisions) + provisionee = new Provisionee(new File(jsonParam)); + provisionee.saveJson(true); + } else { + // Process single provision step (preserves existing provisions) + provisionee = new Provisionee( + valueOf("--type"), + valueOpt("--phase"), + valueOpt("--os"), + valueOf("--data"), + valueOpt("--args"), + valueOpt("--description"), + valuesOpt("--arg%d") + ); + provisionee.saveJson(false); + } + log.info("Successfully added provisioning step(s) {} to file '{}'", provisionee.getJson(), Provisionee.BUILD_PROVISION_FILE); return SUCCESS; default: throw new UnsupportedOperationException("Build type " + argValue + " is not yet supported"); diff --git a/test/qz/build/provision/ProvisionerTests.java b/test/qz/installer/provision/ProvisionerTests.java similarity index 87% rename from test/qz/build/provision/ProvisionerTests.java rename to test/qz/installer/provision/ProvisionerTests.java index ed55065a4..c220609f4 100644 --- a/test/qz/build/provision/ProvisionerTests.java +++ b/test/qz/installer/provision/ProvisionerTests.java @@ -1,7 +1,6 @@ -package qz.build.provision; +package qz.installer.provision; import org.codehaus.jettison.json.JSONException; -import qz.installer.provision.Provisioner; import java.io.IOException; import java.io.InputStream; diff --git a/test/qz/build/provision/resources/cert1.crt b/test/qz/installer/provision/resources/cert1.crt similarity index 100% rename from test/qz/build/provision/resources/cert1.crt rename to test/qz/installer/provision/resources/cert1.crt diff --git a/test/qz/build/provision/resources/provision.json b/test/qz/installer/provision/resources/provision.json similarity index 95% rename from test/qz/build/provision/resources/provision.json rename to test/qz/installer/provision/resources/provision.json index 3e4c1ddea..7bcb973a1 100644 --- a/test/qz/build/provision/resources/provision.json +++ b/test/qz/installer/provision/resources/provision.json @@ -47,10 +47,10 @@ "data": "resources/cert1.crt" }, { - "description": "This step will install a property during 'install' (implied) phase to the qz-tray.properties on all OSs", + "description": "This step will install a property during 'certgen' (implied) phase to the qz-tray.properties on all OSs", "type": "property", "os": "*", - "data": "wss.host=0.0.0.0" + "data": "log.size=2097152" }, { "description": "This step will install a property during 'startup' (implied) phase to the prefs.properties on all OSs", diff --git a/test/qz/build/provision/resources/script1.ps1 b/test/qz/installer/provision/resources/script1.ps1 similarity index 100% rename from test/qz/build/provision/resources/script1.ps1 rename to test/qz/installer/provision/resources/script1.ps1 diff --git a/test/qz/build/provision/resources/script2 b/test/qz/installer/provision/resources/script2 similarity index 100% rename from test/qz/build/provision/resources/script2 rename to test/qz/installer/provision/resources/script2 diff --git a/test/qz/build/provision/resources/script3.sh b/test/qz/installer/provision/resources/script3.sh similarity index 100% rename from test/qz/build/provision/resources/script3.sh rename to test/qz/installer/provision/resources/script3.sh