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