From ad19600085154d5146223376aa2ebe854c35cb0a Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Tue, 8 Oct 2019 00:25:37 +0200 Subject: [PATCH 01/53] Minimum viable product. And oh boy, it's minimum, all right. It's "okay, you're not fired, but you're on THIN ICE, mister!" minimum. But it's not completely broken, so i'm committing it, so i can always go back to this and remember these simple times. --- bin/snap-wrapper | 2 +- package.json | 3 +- src/devices.js | 230 ++++++++++++++++------------------- src/html/index.pug | 1 + src/html/scripts/main.pug | 11 +- src/html/views/select-os.pug | 15 +++ src/main.js | 31 +++-- 7 files changed, 158 insertions(+), 135 deletions(-) create mode 100644 src/html/views/select-os.pug diff --git a/bin/snap-wrapper b/bin/snap-wrapper index f0d8c878..c8b2d644 100755 --- a/bin/snap-wrapper +++ b/bin/snap-wrapper @@ -3,4 +3,4 @@ export FONTCONFIG_PATH=$SNAP/etc/fonts export FONTCONFIG_FILE=$SNAP/etc/fonts/fonts.conf export XDG_DATA_HOME=$SNAP/usr/share export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$SNAP/usr/lib/x86_64-linux-gnu/ -exec "$SNAP/app/ubports-installer" +exec "$SNAP/app/ubports-installer" "$@" diff --git a/package.json b/package.json index 3c82eec2..c369f710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ubports-installer", - "version": "0.3.2-beta", + "version": "0.4.0-beta", "description": "The easy way to install Ubuntu Touch on UBports devices. A friendly cross-platform Installer for Ubuntu Touch. Just connect a supported device to your PC, follow the on-screen instructions and watch this awesome tool do all the rest.", "keywords": [ "Ubuntu", @@ -54,7 +54,6 @@ "promise-android-tools": "^1.0.2", "request": "^2.79.0", "system-image-node-module": "^1.0.9", - "ubports-api-node-module": "^1.0.2", "winston": "^2.3.1" } } diff --git a/src/devices.js b/src/devices.js index 6f8b7e3d..3b43ed14 100644 --- a/src/devices.js +++ b/src/devices.js @@ -18,13 +18,11 @@ */ const http = require("request"); -const ubportsApi = require("ubports-api-node-module"); const systemImage = require("./system-image"); const utils = require("./utils"); const os = require("os"); const path = require("path"); -const devicesApi = new ubportsApi.Devices(); const downloadPath = utils.getUbuntuTouchDir(); // HACK: This should be handled by the server, not locally @@ -127,6 +125,31 @@ function addPathToImages(images, device) { return ret; } +function sysimageinstall(instructs) { + return new Promise(function(resolve, reject) { + adb.waitForDevice(5000).then(() => { + console.log("adb device detected") + sic.downloadLatestVersion({device: "hammerhead", channel: "ubports-touch/16.04/edge", wipe: "false"}, downloadSpeed, downloadNext).then((files) => { + console.log("Download done"); + adb.wipeCache().then(() => { + adb.shell("mount -a").then(() => { + adb.shell("mkdir -p /cache/recovery").then(() => { + adb.pushArray(files, (progress) => { + console.log("push progress: " + progress*100); + }).then(() => { + adb.reboot("recovery").then(() => { + console.log("reboot successfull"); + resolve(); + }).catch(e => die("Reboot failed: " + e)); + }).catch(e => die("Push failed: Failed push: " + e)); + }).catch(e => die("Push failed: Failed to create target dir: " + e)); + }).catch(e => die("Push failed: Failed to mount: " + e)); + }).catch(e => die("Push failed: Failed to wipe cache: " + e)); + }).catch(e => die("System-Image Download failed: " + e)); + }).catch(e => die("Wait-for-device error: " + e)); + }); +} + global.mainEvent.on("download:progress", (percent) => { global.mainEvent.emit("user:write:progress", percent*100); }); @@ -134,91 +157,71 @@ global.mainEvent.on("download:speed", (speed) => { global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); }); -var install = (options) => { - if (!options) - return false; - devicesApi.getInstallInstructs(options.device).then((instructs) => { - global.mainEvent.once("adbpush:done", () => { - utils.log.info("Done pushing files"); - utils.log.info("Rebooting to recovery to flash"); - global.mainEvent.emit("user:write:progress", 0); - global.mainEvent.emit("user:write:working", "particles"); - instructReboot("recovery", instructs.buttons, () => { - global.mainEvent.emit("user:write:done"); - }); - }); - global.mainEvent.once("bootstrap:done", (bootstrap) => { - utils.log.info("bootstrap done: " + (bootstrap ? "rebooting automatically" : "rebooting manually")); - if (!bootstrap) { - instructReboot("recovery", instructs.buttons, () => { - systemImage.installLatestVersion({ - device: options.device, - channel: options.channel, - wipe: options.wipe - }); +function install(steps) { + var installPromises = [] + + steps.forEach((step) => { + switch (step.type) { + case "download": + installPromises.push(() => { + utils.log.debug(step.type); + return utils.downloadFiles(addPathToImages(step.files, step.group), ()=>{}, ()=>{}); }); - } else { - global.mainEvent.emit("user:write:status", "Rebooting to recovery"); - global.mainEvent.emit("user:write:under", "Waiting for device to enter recovery mode"); - adb.waitForDevice().then(() => { - systemImage.installLatestVersion({ - device: options.device, - channel: options.channel, - wipe: options.wipe - }); - }).catch((error) => { utils.errorToUser(error, "Wait for device"); }); - } - }); - if (instructs.images.length > 0) { // If images are specified, flash them (bootstrapping) - // We need to be in bootloader - global.mainEvent.emit("user:write:status", "Waiting for device to enter bootloader mode"); - global.mainEvent.emit("user:write:under", "Fastboot is scanning for devices"); - instructReboot("bootloader", instructs.buttons, () => { - global.mainEvent.once("download:done", () => { - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Flashing images"); - global.mainEvent.emit("user:write:under", "Flashing recovery and boot images"); - global.mainEvent.emit("user:write:progress", 0); - fastboot.erase("cache").then(() => { - fastboot.flashArray(addPathToImages(instructs.images, options.device)).then(() => { - if (instructs.bootstrap) { // Device should support the fastboot boot command - var recoveryImg; - instructs.images.forEach((image) => { - if (image.type == "recovery") recoveryImg = image.file; - }); - fastboot.boot(recoveryImg).then(() => { - global.mainEvent.emit("bootstrap:done", true); - }).catch(() => { - global.mainEvent.emit("bootstrap:done", false); - }); - } else { - global.mainEvent.emit("bootstrap:done", false); - } - }).catch((error) => { utils.errorToUser(error, "bootstrap"); }); - }).catch(((e) => { utils.errorToUser(e, "Fastboot: Erase cache"); })); + break; + case "adb:reboot": + installPromises.push(() => { + utils.log.debug(step.type); + return adb.reboot(step.to_state); }); - downloadImages(instructs.images, options.device); - }); - } else { // If no images are specified, go straight to system-image - // We need to be in recovery - instructReboot("recovery", instructs.buttons, () => { - systemImage.installLatestVersion({ - device: options.device, - channel: options.channel, - wipe: options.wipe + break; + case "fastboot:flash": + installPromises.push(() => { + utils.log.debug(step.type); + return fastboot.flashArray(addPathToImages(step.flash)); }); - }); + break; + case "fastboot:erase": + installPromises.push(() => { + utils.log.debug(step.type); + return fastboot.erase(step.partition); + }); + break; + case "fastboot:boot": + installPromises.push(() => { + utils.log.debug(step.type); + return fastboot.boot(path.join("./", "test", step.group, step.file), step.partition); + }); + break; + case "systemimage": + installPromises.push(() => { + utils.log.debug(step.type); + return sysimageinstall(); + }); + break; + case "fastboot:update": + installPromises.push(() => { + utils.log.debug(step.type); + return fastboot.update(path.join("./", "test", step.group, step.file)); + }); + break; + default: + throw "error: unrecognized step type: " + step.type } - }).catch((e) => { utils.errorToUser(e, "Install"); }); + }); + + installPromises.reduce( + (promiseChain, currentFunction) => promiseChain.then(currentFunction), + Promise.resolve() + ); } module.exports = { - getDevice: devicesApi.getDevice, + getDevice: undefined, waitForDevice: () => { adb.waitForDevice().then(() => { adb.getDeviceName().then((device) => { adb.getOs().then((operatingSystem) => { - global.mainEvent.emit("device:select:event", device, (operatingSystem=="ubuntutouch"), true); + global.mainEvent.emit("device:detected", device, (operatingSystem=="ubuntutouch"), true); return; }).catch((error) => { utils.errorToUser(error, "Wait for device") @@ -228,58 +231,41 @@ module.exports = { }); }).catch(e => utils.log.debug("no device detected: " + e)); global.mainEvent.once("device:select", (device) => { - global.mainEvent.emit("stop"); - utils.log.info(device + " selected"); + adb.stopWaiting(); + utils.log.info(device.name + "(" + device.codename + ")" + " selected"); global.mainEvent.emit("device:select:event", device, false, false); }); global.mainEvent.once("device:select:event", (device, ubuntuCom, autoDetected) => { - devicesApi.getDevice(device).then((apiData) => { - if (apiData) { - systemImage.getDeviceChannels(device).then((channels) => { - var channelsAppend = []; - devicesApi.getInstallInstructs(device).then((ret) => { - channels.forEach((channel) => { - var _channel = channel.replace("ubports-touch/", ""); - // Ignore blacklisted channels - if (ret["systemServer"]["blacklist"].indexOf(channel) == -1 && - channel.indexOf("15.04") == -1) { - if (channel === ret["systemServer"]["selected"]) - channelsAppend.push(""); - else - channelsAppend.push(""); - } - }); - channelsAppend.push("") - global.mainEvent.emit("device:select:data-ready", apiData, device, channelsAppend.join(''), ubuntuCom, autoDetected); - }).catch(((e) => { utils.log.error(e); global.mainEvent.emit("user:no-network"); })); - }).catch((e) => { utils.log.error(e); global.mainEvent.emit("user:no-network"); }); - } else { - mainEvent.emit("user:device-unsupported", device); // If there is no response, the device is not supported - return; - } - }).catch(((e) => { utils.errorToUser(e, "Device Select"); })); + // FIXME: Implement settings, don't just install + install(device.steps); }); }, - install: install, getDeviceSelects: (callback) => { - devicesApi.getDevices().then((devices) => { - if (devices) { - var devicesAppend = []; - devices.sort(function(a, b){ - var y = a.name.toLowerCase(); - var x = b.name.toLowerCase(); - if (x < y) {return 1;} - if (x > y) {return -1;} - return 0; - }); - devices.forEach((device) => { - devicesAppend.push(""); - }); - utils.log.debug("Successfully downloaded devices list"); - callback(devicesAppend.join('')); - } else { - callback(false); + var devices = [ + { + device: "hammerhead", + name: "Nexus 5" } - }).catch(((e) => { utils.errorToUser(e, "getDeviceSelects"); })); + ] + var devicesAppend = []; + devices.sort(function(a, b){ + var y = a.name.toLowerCase(); + var x = b.name.toLowerCase(); + if (x < y) {return 1;} + if (x > y) {return -1;} + return 0; + }); + devices.forEach((device) => { + devicesAppend.push(""); + }); + utils.log.debug("Successfully downloaded devices list"); + callback(devicesAppend.join('')); + }, + getOsSelects: (osArray) => { + var osSelects = []; + for (var i = 0; i < osArray.length; i++) { + osSelects.push(""); + } + return osSelects; } } diff --git a/src/html/index.pug b/src/html/index.pug index 652ccf70..f64c4bfd 100644 --- a/src/html/index.pug +++ b/src/html/index.pug @@ -19,6 +19,7 @@ html include views/done include views/reboot include views/install + include views/select-os include views/not-supported include views/wait-for-device include views/working diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 13ba5f05..4db50ac9 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -104,6 +104,13 @@ script. $("#btn-inst").hide(); }); + ipcRenderer.on("user:os", (event, installConfig, osSelects) => { + global.installConfig = installConfig; + global.installConfig.os_to_install = undefined; + $("#options-os").append(osSelects); + views.show("select-os"); + }); + ipcRenderer.on("device:select:data-ready", (event, output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { views.show("install"); $("#options-channel").append(channels); @@ -128,7 +135,7 @@ script. $("#btn-installModal").click(() => { views.show("working", "particles"); $("#progress").show(); - ipcRenderer.send("user:device:select", { + ipcRenderer.send("install", { device: output.device, channel: options.get("channel", true), wipe: options.get("wipe", true) @@ -153,7 +160,7 @@ script. // Button to confirm device selection $("#btn-select-device").click(() => { var device = $("#device-select").find(":selected").attr("name"); - ipcRenderer.send("device:select", device); + ipcRenderer.send("device:selected", device); }); }); diff --git a/src/html/views/select-os.pug b/src/html/views/select-os.pug new file mode 100644 index 00000000..fd7e56f6 --- /dev/null +++ b/src/html/views/select-os.pug @@ -0,0 +1,15 @@ +#views-select-os.main.container.views(hidden='hidden') + .row + .col-xs-6 + img(style='height: 350px; margin: auto; display: block;', src='../screens/Screen6.jpg') + .col-xs-6(style='height: 100%') + h4.user-install-header(style='font-weight: bold;') Select your Operating System + p + | Your device: #[a#your-ubp-device] + p + | What operating system do you want to install? + form.form-horizontal + .form-group + .col-xs-9 + select#options-os.form-control.space + button#btn-inst.btn.btn-primary(type='button', style='width: 100%; margin-top: 10px;', onclick="ipcRenderer.send(\"user:os:select\", getElementById('options-os').selectedIndex); global.installConfig.os_to_install = getElementById('options-os').selectedIndex;") Install diff --git a/src/main.js b/src/main.js index c6f3aca6..2032c8f8 100755 --- a/src/main.js +++ b/src/main.js @@ -61,20 +61,29 @@ var fastboot = new Fastboot({ }); global.fastboot = fastboot; +//============================================================================== +// PARSE COMMAND-LINE ARGUMENTS +//============================================================================== + cli .name(global.packageInfo.name) .version(global.packageInfo.version) .description(global.packageInfo.description) .option('-d, --device ', '[experimental] Override detected device-id (codename)') - .option('-c, --channel ', '[experimental] Override the recommended release-channel for the device') - .option('-C, --cli', "[experimental] Run without GUI", undefined, 'false') + .option('-o, --operating-system ', '[experimental] what os to install') + .option('-s, --settings ": [, ...]"', '[experimental] Override install settings') + .option('-f, --file ', '[experimental] Override the config by loading a file') + .option('-c, --cli', "[experimental] Run without GUI", undefined, 'false') .option('-v, --verbose', "Enable verbose logging", undefined, 'false') .option('-D, --debug', "Enable debugging tools and verbose logging", undefined, 'false') .parse(process.argv); +if (cli.file) { + global.installConfig = require(cli.file); +} + global.installProperties = { - device: cli.device, - channel: cli.channel, + device: global.installConfig ? global.installConfig.codename : cli.device, cli: cli.cli, verbose: (cli.verbose || cli.debug), debug: cli.debug @@ -89,9 +98,9 @@ utils.exportExecutablesFromPackage(); //============================================================================== // Device selected -ipcMain.on("user:device:select", (event, installProperties) => { +ipcMain.on("install", (event, installProperties) => { global.installProperties = Object.assign(global.installProperties, installProperties); - devices.install(installProperties); + mainEvent.emit("select:os"); }); // Exit process with optional non-zero exit code @@ -117,9 +126,15 @@ ipcMain.on("createBugReport", (event, title) => { }); // The user selected a device -ipcMain.on("device:select", (event, device) => { +ipcMain.on("device:selected", (event, device) => { global.installProperties.device = device; - mainEvent.emit("device:select", device); + mainWindow.webContents.send("user:os", global.installConfig, devices.getOsSelects(global.installConfig.operating_systems)); +}); + +// The user selected an os +ipcMain.on("user:os:select", (event, osIndex) => { + utils.log.debug(global.installConfig.operating_systems[osIndex]); + mainEvent.emit("device:select", global.installConfig.operating_systems[osIndex]); }); //============================================================================== From b0e673e2b3bd4979ab1b2af01be42bb9fd4345e8 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Tue, 8 Oct 2019 13:31:39 +0200 Subject: [PATCH 02/53] Basic implementation of ui events without any proper logic --- src/devices.js | 103 +++++++++++++++++++++++++++++++------- src/html/scripts/main.pug | 8 ++- src/main.js | 5 +- 3 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/devices.js b/src/devices.js index 3b43ed14..c5a72e08 100644 --- a/src/devices.js +++ b/src/devices.js @@ -114,12 +114,12 @@ var downloadImages = (images, device) => { }); } -function addPathToImages(images, device) { +function addPathToImages(images, device, group) { var ret = []; images.forEach((image) => { image["partition"] = image.type; - image["path"] = path.join(downloadPath, "images", device); - image["file"] = path.join(downloadPath, "images", device, path.basename(image.url)); + image["path"] = path.join(downloadPath, "images", device, group); + image["file"] = path.join(downloadPath, "images", device, group, path.basename(image.url)); ret.push(image); }); return ret; @@ -164,51 +164,120 @@ function install(steps) { switch (step.type) { case "download": installPromises.push(() => { - utils.log.debug(step.type); - return utils.downloadFiles(addPathToImages(step.files, step.group), ()=>{}, ()=>{}); + // return utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), ()=>{}, ()=>{}); + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "download");+ + global.mainEvent.emit("user:write:status", "Downloading " + step.group); + global.mainEvent.emit("user:write:under", "Downloading"); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "adb:reboot": installPromises.push(() => { - utils.log.debug(step.type); - return adb.reboot(step.to_state); + // return adb.reboot(step.to_state); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "fastboot:flash": installPromises.push(() => { - utils.log.debug(step.type); - return fastboot.flashArray(addPathToImages(step.flash)); + // return fastboot.flashArray(addPathToImages(step.flash, global.installProperties.device, step.group)); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Flashing firmware"); + global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "fastboot:erase": installPromises.push(() => { - utils.log.debug(step.type); - return fastboot.erase(step.partition); + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Ceaning up"); + global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); + // return fastboot.erase(step.partition); + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "fastboot:boot": installPromises.push(() => { - utils.log.debug(step.type); - return fastboot.boot(path.join("./", "test", step.group, step.file), step.partition); + // return fastboot.boot(path.join("./", "test", step.group, step.file), step.partition); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "systemimage": installPromises.push(() => { - utils.log.debug(step.type); - return sysimageinstall(); + // return sysimageinstall(); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "System-Image"); + global.mainEvent.emit("user:write:under", "Going through system-image process"); + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; case "fastboot:update": installPromises.push(() => { - utils.log.debug(step.type); - return fastboot.update(path.join("./", "test", step.group, step.file)); + // return fastboot.update(path.join("./", "test", step.group, step.file)); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Updating system"); + global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); + utils.log.debug("step: " + JSON.stringify(step)); + setTimeout(() => { + utils.log.debug(step.type + " done"); + resolve(); + }, 2000); + }); }); break; default: throw "error: unrecognized step type: " + step.type } }); + installPromises.push(() => { + global.mainEvent.emit("user:write:done"); + global.mainEvent.emit("user:write:status", global.installConfig.operating_systems[global.installProperties.osIndex].name + " successfully installed!", false); + global.mainEvent.emit("user:write:under", global.installConfig.operating_systems[global.installProperties.osIndex].success_message || "All done! Enjoy exploring your new OS!"); + }); + // Actually run the steps installPromises.reduce( (promiseChain, currentFunction) => promiseChain.then(currentFunction), Promise.resolve() diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 4db50ac9..81a6abfd 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -82,18 +82,16 @@ script. remote.getCurrentWindow().close(); } else { views.show("done"); - footer.topText.set("Installation complete!"); - footer.underText.set("It is now safe to unplug the device."); $("#progress").width("0%"); } }); - ipcRenderer.on("user:write:status", (e, status) => { - footer.topText.set(status, true) + ipcRenderer.on("user:write:status", (e, status, waitDots) => { + footer.topText.set(status, waitDots); }); ipcRenderer.on("user:write:under", (e, status) => { - footer.underText.set(status, true) + footer.underText.set(status, true); }); ipcRenderer.on("user:device-unsupported", (event, device) => { diff --git a/src/main.js b/src/main.js index 2032c8f8..8a324c04 100755 --- a/src/main.js +++ b/src/main.js @@ -133,6 +133,7 @@ ipcMain.on("device:selected", (event, device) => { // The user selected an os ipcMain.on("user:os:select", (event, osIndex) => { + global.installProperties.osIndex = osIndex; utils.log.debug(global.installConfig.operating_systems[osIndex]); mainEvent.emit("device:select", global.installConfig.operating_systems[osIndex]); }); @@ -210,8 +211,8 @@ mainEvent.on("user:write:working", (animation) => { }); // Set the top text in the footer -mainEvent.on("user:write:status", (status) => { - if (mainWindow) mainWindow.webContents.send("user:write:status", status); +mainEvent.on("user:write:status", (status, waitDots) => { + if (mainWindow) mainWindow.webContents.send("user:write:status", status, waitDots); }); // Set the speed part of the footer From b9d41e94bd01508b063831ef2acc33a181f56957 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 9 Oct 2019 18:34:02 +0200 Subject: [PATCH 03/53] Begin implementation of new new api module (2.0.x) --- src/devices.js | 46 ++++++--------------------------------- src/html/scripts/main.pug | 3 --- src/main.js | 30 +++++++++++++------------ 3 files changed, 23 insertions(+), 56 deletions(-) diff --git a/src/devices.js b/src/devices.js index c5a72e08..c356d3bd 100644 --- a/src/devices.js +++ b/src/devices.js @@ -290,45 +290,12 @@ module.exports = { adb.waitForDevice().then(() => { adb.getDeviceName().then((device) => { adb.getOs().then((operatingSystem) => { - global.mainEvent.emit("device:detected", device, (operatingSystem=="ubuntutouch"), true); - return; - }).catch((error) => { - utils.errorToUser(error, "Wait for device") - }); - }).catch((error) => { - utils.errorToUser(error, "get device name"); - }); + global.api.resolveAlias(device).then((resolvedDevice) => { + global.mainEvent.emit("device:detected", resolvedDevice, (operatingSystem=="ubuntutouch"), true); + }).catch((error) => { utils.errorToUser(error, "Resolve device alias"); }); + }).catch((error) => { utils.errorToUser(error, "Wait for device"); }); + }).catch((error) => { utils.errorToUser(error, "get device name"); }); }).catch(e => utils.log.debug("no device detected: " + e)); - global.mainEvent.once("device:select", (device) => { - adb.stopWaiting(); - utils.log.info(device.name + "(" + device.codename + ")" + " selected"); - global.mainEvent.emit("device:select:event", device, false, false); - }); - global.mainEvent.once("device:select:event", (device, ubuntuCom, autoDetected) => { - // FIXME: Implement settings, don't just install - install(device.steps); - }); - }, - getDeviceSelects: (callback) => { - var devices = [ - { - device: "hammerhead", - name: "Nexus 5" - } - ] - var devicesAppend = []; - devices.sort(function(a, b){ - var y = a.name.toLowerCase(); - var x = b.name.toLowerCase(); - if (x < y) {return 1;} - if (x > y) {return -1;} - return 0; - }); - devices.forEach((device) => { - devicesAppend.push(""); - }); - utils.log.debug("Successfully downloaded devices list"); - callback(devicesAppend.join('')); }, getOsSelects: (osArray) => { var osSelects = []; @@ -336,5 +303,6 @@ module.exports = { osSelects.push(""); } return osSelects; - } + }, + install: install } diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 81a6abfd..0f1172ef 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -145,9 +145,6 @@ script. ipcRenderer.on("device:wait:device-selects-ready", (event, deviceSelects) => { $("#device-select").append(deviceSelects); - }); - - ipcRenderer.on("user:adb:ready", () => { footer.topText.set("Waiting for device", true); footer.underText.set("Please connect your device with a USB cable"); views.show("wait-for-device"); diff --git a/src/main.js b/src/main.js index 8a324c04..3a543b80 100755 --- a/src/main.js +++ b/src/main.js @@ -27,6 +27,7 @@ global.packageInfo = require('../package.json'); const Adb = require('promise-android-tools').Adb; const Fastboot = require('promise-android-tools').Fastboot; +const Api = require("../../ubports-api-node-module/src/module.js").Installer; const exec = require('child_process').exec; const path = require('path'); @@ -44,6 +45,8 @@ global.mainEvent = mainEvent; const utils = require('./utils.js'); global.utils = utils; const devices = require('./devices.js'); +const api = new Api(); +global.api = api; var adb = new Adb({ exec: (args, callback) => { exec( [(path.join(utils.getUbuntuTouchDir(), 'platform-tools', 'adb'))].concat(args).join(" "), @@ -97,12 +100,6 @@ utils.exportExecutablesFromPackage(); // RENDERER SIGNAL HANDLING //============================================================================== -// Device selected -ipcMain.on("install", (event, installProperties) => { - global.installProperties = Object.assign(global.installProperties, installProperties); - mainEvent.emit("select:os"); -}); - // Exit process with optional non-zero exit code ipcMain.on("die", (exitCode) => { process.exit(exitCode); @@ -127,6 +124,7 @@ ipcMain.on("createBugReport", (event, title) => { // The user selected a device ipcMain.on("device:selected", (event, device) => { + adb.stopWaiting(); global.installProperties.device = device; mainWindow.webContents.send("user:os", global.installConfig, devices.getOsSelects(global.installConfig.operating_systems)); }); @@ -135,7 +133,7 @@ ipcMain.on("device:selected", (event, device) => { ipcMain.on("user:os:select", (event, osIndex) => { global.installProperties.osIndex = osIndex; utils.log.debug(global.installConfig.operating_systems[osIndex]); - mainEvent.emit("device:select", global.installConfig.operating_systems[osIndex]); + devices.install(global.installConfig.operating_systems[osIndex].steps); }); //============================================================================== @@ -237,9 +235,11 @@ mainEvent.on("device:select:data-ready", (output, device, channels, ubuntuCom, a if (mainWindow) mainWindow.webContents.send("device:select:data-ready", output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid); }); -// No internet connection -mainEvent.on("user:no-network", () => { - if (mainWindow) mainWindow.webContents.send("user:no-network"); +// The user selected a device +mainEvent.on("device:detected", (device) => { + utils.log.info("device detected: " + device) + global.installProperties.device = device; + mainWindow.webContents.send("user:os", global.installConfig, devices.getOsSelects(global.installConfig.operating_systems)); }); //============================================================================== @@ -261,11 +261,13 @@ function createWindow () { // Tasks we need for every start mainWindow.webContents.on("did-finish-load", () => { adb.startServer().then(() => { - mainWindow.webContents.send("user:adb:ready"); devices.waitForDevice(); - }); - devices.getDeviceSelects((out) => { - mainWindow.webContents.send("device:wait:device-selects-ready", out) + }).catch(e => utils.errorToUser("Failed to start adb server: " + e)); + api.getDeviceSelects().then((out) => { + mainWindow.webContents.send("device:wait:device-selects-ready", out); + }).catch(e => { + utils.log.error("getDeviceSelects error: " + e) + mainWindow.webContents.send("user:no-network"); }); }); From 08ac999f163af80201d93c7e546374c849a03102 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 9 Oct 2019 18:49:21 +0200 Subject: [PATCH 04/53] Straighten out some events --- src/devices.js | 3 +-- src/html/scripts/main.pug | 2 +- src/html/views/select-os.pug | 2 +- src/main.js | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/devices.js b/src/devices.js index c356d3bd..bc25dc4a 100644 --- a/src/devices.js +++ b/src/devices.js @@ -285,7 +285,6 @@ function install(steps) { } module.exports = { - getDevice: undefined, waitForDevice: () => { adb.waitForDevice().then(() => { adb.getDeviceName().then((device) => { @@ -297,7 +296,7 @@ module.exports = { }).catch((error) => { utils.errorToUser(error, "get device name"); }); }).catch(e => utils.log.debug("no device detected: " + e)); }, - getOsSelects: (osArray) => { + getOsSelects: (osArray) => { // TODO move to api module var osSelects = []; for (var i = 0; i < osArray.length; i++) { osSelects.push(""); diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 0f1172ef..852fc1cc 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -109,7 +109,7 @@ script. views.show("select-os"); }); - ipcRenderer.on("device:select:data-ready", (event, output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { + ipcRenderer.on("user:configure", (event, output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { views.show("install"); $("#options-channel").append(channels); options.optionsValToUser(); diff --git a/src/html/views/select-os.pug b/src/html/views/select-os.pug index fd7e56f6..56eb1f79 100644 --- a/src/html/views/select-os.pug +++ b/src/html/views/select-os.pug @@ -12,4 +12,4 @@ .form-group .col-xs-9 select#options-os.form-control.space - button#btn-inst.btn.btn-primary(type='button', style='width: 100%; margin-top: 10px;', onclick="ipcRenderer.send(\"user:os:select\", getElementById('options-os').selectedIndex); global.installConfig.os_to_install = getElementById('options-os').selectedIndex;") Install + button#btn-inst.btn.btn-primary(type='button', style='width: 100%; margin-top: 10px;', onclick="ipcRenderer.send(\"os:selected\", getElementById('options-os').selectedIndex); global.installConfig.os_to_install = getElementById('options-os').selectedIndex;") Install diff --git a/src/main.js b/src/main.js index 3a543b80..863a0dcf 100755 --- a/src/main.js +++ b/src/main.js @@ -130,7 +130,7 @@ ipcMain.on("device:selected", (event, device) => { }); // The user selected an os -ipcMain.on("user:os:select", (event, osIndex) => { +ipcMain.on("os:selected", (event, osIndex) => { global.installProperties.osIndex = osIndex; utils.log.debug(global.installConfig.operating_systems[osIndex]); devices.install(global.installConfig.operating_systems[osIndex].steps); @@ -230,9 +230,9 @@ mainEvent.on("user:device-unsupported", (device) => { }); // Set the install configuration data -mainEvent.on("device:select:data-ready", (output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { +mainEvent.on("user:configure", (output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { global.installProperties.device = device; - if (mainWindow) mainWindow.webContents.send("device:select:data-ready", output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid); + if (mainWindow) mainWindow.webContents.send("user:configure", output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid); }); // The user selected a device From 615e0f0638775d397889f3873aa0974ca75bb632 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 10 Oct 2019 02:33:52 +0200 Subject: [PATCH 05/53] Fix install process up to fastboot flash --- src/devices.js | 76 ++++++++++++++++++++------------------- src/html/scripts/main.pug | 2 ++ src/main.js | 4 +-- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/devices.js b/src/devices.js index bc25dc4a..a3142848 100644 --- a/src/devices.js +++ b/src/devices.js @@ -95,36 +95,25 @@ var instructReboot = (state, button, callback) => { }); } -var downloadImages = (images, device) => { - utils.log.debug(addPathToImages(images, device)); - global.mainEvent.emit("user:write:working", "download"); - global.mainEvent.emit("user:write:status", "Downloading Firmware"); - global.mainEvent.emit("user:write:under", "Downloading"); - utils.downloadFiles(images, (progress, speed) => { - global.mainEvent.emit("download:progress", progress); - global.mainEvent.emit("download:speed", speed); - }, (current, total) => { - if (current != total) utils.log.debug("Downloading bootstrap image " + (current+1) + " of " + total); - }).then((files) => { - utils.log.debug(files) - // Wait for one second until the progress event stops firing - setTimeout(() => { - global.mainEvent.emit("download:done"); - }, 1000); - }); -} - function addPathToImages(images, device, group) { var ret = []; images.forEach((image) => { image["partition"] = image.type; - image["path"] = path.join(downloadPath, "images", device, group); - image["file"] = path.join(downloadPath, "images", device, group, path.basename(image.url)); + image["path"] = path.join(downloadPath, device, group); + image["file"] = path.join(path.basename(image.url)); ret.push(image); }); return ret; } +function addPathToFiles(files, device) { + var ret = []; + for (var i = 0; i < files.length; i++) { + ret.push({file: path.join(downloadPath, device, files[i].group, files[i].file), partition: files[i].partition }); + } + return ret; +} + function sysimageinstall(instructs) { return new Promise(function(resolve, reject) { adb.waitForDevice(5000).then(() => { @@ -158,52 +147,65 @@ global.mainEvent.on("download:speed", (speed) => { }); function install(steps) { - var installPromises = [] - + var installPromises = []; steps.forEach((step) => { switch (step.type) { case "download": installPromises.push(() => { - // return utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), ()=>{}, ()=>{}); return new Promise(function(resolve, reject) { utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "download");+ + global.mainEvent.emit("user:write:working", "download"); global.mainEvent.emit("user:write:status", "Downloading " + step.group); global.mainEvent.emit("user:write:under", "Downloading"); - setTimeout(() => { + utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { + global.mainEvent.emit("user:write:progress", progress*100); + global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); + }, (current, total) => { + utils.log.info("Downloaded file " + current + " of " + total); + }).then(() => { utils.log.debug(step.type + " done"); - resolve(); - }, 2000); + setTimeout(() => { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:under", "Verifying download"); + global.mainEvent.emit("user:write:progress", 0); + global.mainEvent.emit("user:write:speed", 0); + resolve(); + }, 1000); + }); }); }); break; case "adb:reboot": installPromises.push(() => { - // return adb.reboot(step.to_state); return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); global.mainEvent.emit("user:write:working", "particles");+ global.mainEvent.emit("user:write:status", "Rebooting"); global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); - utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + adb.reboot(step.to_state).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch((error) => { + utils.log.error("reboot failed: " + error); + // TODO instructReboot(step.to_state, undefined, resolve); + utils.log.info("REBOOT MANUALLY"); + resolve(); + }); }); }); break; case "fastboot:flash": installPromises.push(() => { - // return fastboot.flashArray(addPathToImages(step.flash, global.installProperties.device, step.group)); return new Promise(function(resolve, reject) { - global.mainEvent.emit("user:write:working", "particles");+ + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Flashing firmware"); global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); - utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) + fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch(reject); }); }); break; diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 852fc1cc..4ce9af0a 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -71,9 +71,11 @@ script. }); ipcRenderer.on("user:write:progress", (e, length) => { + console.log(length) if(length >= 100) { length=100; } + $("#progress").show(); $("#progress").width(length.toString()+"%"); }); diff --git a/src/main.js b/src/main.js index 863a0dcf..6d5dc521 100755 --- a/src/main.js +++ b/src/main.js @@ -192,8 +192,8 @@ mainEvent.on("reboot:done", () => { }); // Control the progress bar -mainEvent.on("user:write:progress", (length) => { - if (mainWindow) mainWindow.webContents.send("user:write:progress", length); +mainEvent.on("user:write:progress", (progress) => { + if (mainWindow) mainWindow.webContents.send("user:write:progress", progress); }); // Installation successfull From 66ec4065fa5815f1de06dfab03b9804a92c803be Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 10 Oct 2019 09:35:02 +0200 Subject: [PATCH 06/53] It worked. It worked! They said I was MAD, but it actually WORKED! --- src/devices.js | 66 +++++++++------------------------------ src/html/scripts/main.pug | 1 - src/system-image.js | 52 +++++++++++++++--------------- 3 files changed, 42 insertions(+), 77 deletions(-) diff --git a/src/devices.js b/src/devices.js index a3142848..7721683b 100644 --- a/src/devices.js +++ b/src/devices.js @@ -114,38 +114,6 @@ function addPathToFiles(files, device) { return ret; } -function sysimageinstall(instructs) { - return new Promise(function(resolve, reject) { - adb.waitForDevice(5000).then(() => { - console.log("adb device detected") - sic.downloadLatestVersion({device: "hammerhead", channel: "ubports-touch/16.04/edge", wipe: "false"}, downloadSpeed, downloadNext).then((files) => { - console.log("Download done"); - adb.wipeCache().then(() => { - adb.shell("mount -a").then(() => { - adb.shell("mkdir -p /cache/recovery").then(() => { - adb.pushArray(files, (progress) => { - console.log("push progress: " + progress*100); - }).then(() => { - adb.reboot("recovery").then(() => { - console.log("reboot successfull"); - resolve(); - }).catch(e => die("Reboot failed: " + e)); - }).catch(e => die("Push failed: Failed push: " + e)); - }).catch(e => die("Push failed: Failed to create target dir: " + e)); - }).catch(e => die("Push failed: Failed to mount: " + e)); - }).catch(e => die("Push failed: Failed to wipe cache: " + e)); - }).catch(e => die("System-Image Download failed: " + e)); - }).catch(e => die("Wait-for-device error: " + e)); - }); -} - -global.mainEvent.on("download:progress", (percent) => { - global.mainEvent.emit("user:write:progress", percent*100); -}); -global.mainEvent.on("download:speed", (speed) => { - global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); -}); - function install(steps) { var installPromises = []; steps.forEach((step) => { @@ -205,67 +173,63 @@ function install(steps) { fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { utils.log.debug(step.type + " done"); resolve(); - }).catch(reject); + }).catch(e => errorToUser(e, "fastboot flash")); }); }); break; case "fastboot:erase": installPromises.push(() => { - global.mainEvent.emit("user:write:working", "particles");+ - global.mainEvent.emit("user:write:status", "Ceaning up"); - global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); - // return fastboot.erase(step.partition); return new Promise(function(resolve, reject) { utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Ceaning up"); + global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); + fastboot.erase(step.partition).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch(e => errorToUser(e, "fastboot erase")); }); }); break; case "fastboot:boot": installPromises.push(() => { - // return fastboot.boot(path.join("./", "test", step.group, step.file), step.partition); return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Rebooting"); global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); - utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch(e => errorToUser(e, "fastboot boot")); }); }); break; case "systemimage": installPromises.push(() => { - // return sysimageinstall(); return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "System-Image"); global.mainEvent.emit("user:write:under", "Going through system-image process"); - utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + systemImage.installLatestVersion({device: "hammerhead", channel: "ubports-touch/16.04/edge", wipe: "false"}).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch(e => errorToUser(e, "systemimage")); }); }); break; case "fastboot:update": installPromises.push(() => { - // return fastboot.update(path.join("./", "test", step.group, step.file)); return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Updating system"); global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); - utils.log.debug("step: " + JSON.stringify(step)); - setTimeout(() => { + fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { utils.log.debug(step.type + " done"); resolve(); - }, 2000); + }).catch(e => errorToUser(e, "fastboot update")); }); }); break; diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 4ce9af0a..3e4bc849 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -71,7 +71,6 @@ script. }); ipcRenderer.on("user:write:progress", (e, length) => { - console.log(length) if(length >= 100) { length=100; } diff --git a/src/system-image.js b/src/system-image.js index b51fc791..f5da6f4f 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -27,31 +27,33 @@ const getDeviceChannels = (device) => { } var installLatestVersion = (options) => { - mainEvent.emit("user:write:working", "download"); - mainEvent.emit("user:write:status", "Downloading Ubuntu Touch"); - mainEvent.emit("user:write:under", "Downloading"); - systemImage.downloadLatestVersion(options, (progress, speed) => { - mainEvent.emit("download:progress", progress); - mainEvent.emit("download:speed", speed); - }, (current, total) => { - if (current != total) utils.log.debug("Downloading system-image file " + (current+1) + " of " + total); - }).then((files) => { - mainEvent.emit("download:done"); - mainEvent.emit("user:write:progress", 0); - mainEvent.emit("user:write:working", "push"); - mainEvent.emit("user:write:status", "Sending"); - mainEvent.emit("user:write:under", "Sending files to the device"); - adb.wipeCache().then(() => { - adb.shell("mount -a").then(() => { - adb.shell("mkdir -p /cache/recovery").then(() => { - adb.pushArray(files, (progress) => { - global.mainEvent.emit("user:write:progress", progress*100); - }).then(() => { - global.mainEvent.emit("adbpush:done"); - }).catch(e => utils.errorToUser("Push failed: Failed push: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed to create target dir: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed to mount: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed wipe cache: " + e)); + return new Promise(function(resolve, reject) { + mainEvent.emit("user:write:working", "download"); + mainEvent.emit("user:write:status", "Downloading Ubuntu Touch"); + mainEvent.emit("user:write:under", "Downloading"); + systemImage.downloadLatestVersion(options, (progress, speed) => { + mainEvent.emit("user:write:progress", progress*100); + mainEvent.emit("user:write:speed", Math.round(speed*100)/100); + }, (current, total) => { + if (current != total) utils.log.debug("Downloading system-image file " + (current+1) + " of " + total); + }).then((files) => { + mainEvent.emit("download:done"); + mainEvent.emit("user:write:progress", 0); + mainEvent.emit("user:write:working", "push"); + mainEvent.emit("user:write:status", "Sending"); + mainEvent.emit("user:write:under", "Sending files to the device"); + adb.wipeCache().then(() => { + adb.shell("mount -a").then(() => { + adb.shell("mkdir -p /cache/recovery").then(() => { + adb.pushArray(files, (progress) => { + global.mainEvent.emit("user:write:progress", progress*100); + }).then(() => { + resolve(); + }).catch(e => utils.errorToUser("Push failed: Failed push: " + e)); + }).catch(e => utils.errorToUser("Push failed: Failed to create target dir: " + e)); + }).catch(e => utils.errorToUser("Push failed: Failed to mount: " + e)); + }).catch(e => utils.errorToUser("Push failed: Failed wipe cache: " + e)); + }); }); } From ed67e3dcc7ce1e8794465c4bd591fca83d72bbbb Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 10 Oct 2019 13:04:26 +0200 Subject: [PATCH 07/53] Read settings and config file from cli properly --- src/devices.js | 2 +- src/main.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/devices.js b/src/devices.js index 7721683b..3bce3e48 100644 --- a/src/devices.js +++ b/src/devices.js @@ -212,7 +212,7 @@ function install(steps) { global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "System-Image"); global.mainEvent.emit("user:write:under", "Going through system-image process"); - systemImage.installLatestVersion({device: "hammerhead", channel: "ubports-touch/16.04/edge", wipe: "false"}).then(() => { + systemImage.installLatestVersion(Object.assign({device: "hammerhead"}), global.installProperties.settings).then(() => { utils.log.debug(step.type + " done"); resolve(); }).catch(e => errorToUser(e, "systemimage")); diff --git a/src/main.js b/src/main.js index 6d5dc521..12c35a45 100755 --- a/src/main.js +++ b/src/main.js @@ -82,12 +82,13 @@ cli .parse(process.argv); if (cli.file) { - global.installConfig = require(cli.file); + global.installConfig = require(path.join(process.cwd(), cli.file)); } global.installProperties = { device: global.installConfig ? global.installConfig.codename : cli.device, cli: cli.cli, + settings: cli.settings ? JSON.parse(cli.settings) : undefined, verbose: (cli.verbose || cli.debug), debug: cli.debug }; From 12c910442261bb540c1cc49e9c9d7088db865cf9 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Tue, 15 Oct 2019 18:43:31 +0200 Subject: [PATCH 08/53] Add electron-prompt --- package-lock.json | 204 +++++++++++++++++++++------------------------- package.json | 1 + 2 files changed, 92 insertions(+), 113 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b5e485b..2f1804b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ubports-installer", - "version": "0.3.2-beta", + "version": "0.4.0-beta", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1535,6 +1535,14 @@ "sanitize-filename": "^1.6.1" } }, + "doc-ready": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/doc-ready/-/doc-ready-1.0.4.tgz", + "integrity": "sha1-N/U5GWnP+ZQwP9/vLl1QNX+BZNM=", + "requires": { + "eventie": "^1" + } + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -2092,6 +2100,14 @@ } } }, + "electron-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/electron-prompt/-/electron-prompt-1.4.0.tgz", + "integrity": "sha512-z7AHMrixiUArHn/8i9tDC7UlRRLoM50gtREScGASmG67hcI5A5I5paPZsLLgrMoyC5aV9RZ3YsJXHyDI48rM5g==", + "requires": { + "doc-ready": "^1.0.4" + } + }, "electron-publish": { "version": "20.44.4", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-20.44.4.tgz", @@ -2202,6 +2218,11 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, + "eventie": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/eventie/-/eventie-1.0.6.tgz", + "integrity": "sha1-1P/IsMK15JPCqhsiy+kY067nRDc=" + }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -4528,13 +4549,82 @@ "coveralls": "^3.0.0", "mocha": "^4.0.1", "nyc": "^14.1.1", + "sinon": "^7.5.0", "sinon-chai": "^3.3.0" }, "dependencies": { + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==" + }, + "nise": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", + "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + } + }, "sinon-chai": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -5723,118 +5813,6 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, - "ubports-api-node-module": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ubports-api-node-module/-/ubports-api-node-module-1.0.2.tgz", - "integrity": "sha512-ZYYYB+U9Im7u03npqciIYNZ+9OciBP/bYW0teN4NdXpnvmzpIUNxFX6rdLabHdJB8MCn2toLy9FGibBvsHrUbw==", - "requires": { - "chai": "^4.1.2", - "coveralls": "^3.0.0", - "istanbul": "^0.4.5", - "mkdirp": "^0.5.1", - "mocha": "^4.0.1", - "request": "^2.83.0", - "sinon": "^4.1.3", - "sinon-chai": "^2.14.0" - }, - "dependencies": { - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==" - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" - } - }, - "sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", - "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "requires": { - "has-flag": "^2.0.0" - } - } - } - }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index c369f710..5dbcdb66 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "bootstrap": "^3.3.7", "download": "^7.1.0", + "electron-prompt": "^1.4.0", "popper.js": "^1.14.3", "checksum": "^0.1.1", "commander": "^2.9.0", From 8583c638454874b557c502a58ac18ea85c93bde0 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 16 Oct 2019 18:12:12 +0200 Subject: [PATCH 09/53] Add basic dynamic option creation support --- src/html/modals/options.pug | 34 +--------------------------------- src/html/scripts/ui.pug | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 26bb9b81..105ec026 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -7,38 +7,6 @@ h4.modal-title Install options .modal-body div - form.form-horizontal - .form-group - label.col-xs-3.control-label Channel - .col-xs-9 - select#options-channel.form-control.space - .help-block #[a(onclick="shell.openExternal('https://docs.ubports.com/en/latest/about/process/release-schedule.html')") What is this]? - .form-group - .col-xs-3 - label - input#options-wipe(type='checkbox') - | Wipe storage - //- .col-xs-3 - label - input#options-custom-tools(type='checkbox') - | Custom tools - //- .col-xs-12 - p#options-custom-tools-snap-note(hidden='hidden') - b NOTE: - | Custom tools can not be specified for confined snaps. - .col-xs-12 - p#options-wipe-note(hidden='hidden') - b NOTE: - | If the wipe option is enabled all data stored on the device will be erased. - b Please create an external backup of the data you want to keep! - //- .div#options-custom-tools-area(hidden='hidden') - .form-group - label.col-xs-3.control-label adb - .col-xs-9 - input#options-custom-tools-adb.form-control.space(placeholder="path to adb executable") - .form-group - label.col-xs-3.control-label fastboot - .col-xs-9 - input#options-custom-tools-fastboot.form-control.space(placeholder="path to fastboot executable") + form#options-form.form-horizontal .modal-footer button#btn-options-close.btn.btn-default(type='button', data-dismiss='modal') Close diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index 5c3ffb24..a360855a 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -136,6 +136,30 @@ script. } } + function addOption(option) { + let _div = document.createElement("div"); + _div.className = "form-group"; + let _label = document.createElement("label"); + _label.className = "col-xs-3 control-label"; + _label.appendChild(document.createTextNode(option.label)); + _div.appendChild(_label); + + let _subdiv = document.createElement("div"); + _subdiv.className = "col-xs-9"; + + let _input = document.createElement("input"); + _input.className = "form-control space"; + _input.type = option.type; + _input.value = option.value; + _input.id = "options-" + option.id; + + console.log(_input); + console.log(_div); + _subdiv.appendChild(_input); + _div.appendChild(_subdiv); + $("#options-form").append(_div); + } + const getOptionVal = (option, raw) => { var ret = false; optionsVal.forEach((optionVal) => { From a7db3d7fab9254d8de29d87fb6ebfe822ba7332c Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 16 Oct 2019 18:21:36 +0200 Subject: [PATCH 10/53] Don't wait for device if device is already specified --- src/html/scripts/main.pug | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 3e4bc849..d97f3a48 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -145,19 +145,25 @@ script. views.show("working", "particles"); ipcRenderer.on("device:wait:device-selects-ready", (event, deviceSelects) => { - $("#device-select").append(deviceSelects); - footer.topText.set("Waiting for device", true); - footer.underText.set("Please connect your device with a USB cable"); - views.show("wait-for-device"); - // Button to open device selector - $("#btn-modal-select-device").click(() => { - modals.show('select-device'); - }); - // Button to confirm device selection - $("#btn-select-device").click(() => { - var device = $("#device-select").find(":selected").attr("name"); - ipcRenderer.send("device:selected", device); - }); + if (!remote.getGlobal("installProperties").device) { + // if the device is not yet set (i.e. from a cli argument), prompt the user + $("#device-select").append(deviceSelects); + footer.topText.set("Waiting for device", true); + footer.underText.set("Please connect your device with a USB cable"); + views.show("wait-for-device"); + // Button to open device selector + $("#btn-modal-select-device").click(() => { + modals.show('select-device'); + }); + // Button to confirm device selection + $("#btn-select-device").click(() => { + var device = $("#device-select").find(":selected").attr("name"); + ipcRenderer.send("device:selected", device); + }); + } else { + // if the device is set, just return the device:selected event + ipcRenderer.send("device:selected", remote.getGlobal("installProperties").device); + } }); // Assign buttons From 5bda9c8daa742047d56599832da5c7f2ef582f88 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 17 Oct 2019 01:10:25 +0200 Subject: [PATCH 11/53] Improve error handling --- package-lock.json | 38 +++++++++++++++++++------------------- package.json | 2 +- src/devices.js | 7 ++----- src/html/scripts/main.pug | 6 +++--- src/main.js | 22 ++++++++++++++++------ src/system-image.js | 10 +++++----- src/utils.js | 6 +++--- 7 files changed, 49 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f1804b3..7eaa26cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,11 @@ } }, "@babel/generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz", - "integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.4.tgz", + "integrity": "sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w==", "requires": { - "@babel/types": "^7.6.0", + "@babel/types": "^7.6.3", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -73,9 +73,9 @@ } }, "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.4.tgz", + "integrity": "sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A==" }, "@babel/template": { "version": "7.6.0", @@ -88,16 +88,16 @@ } }, "@babel/traverse": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz", - "integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.3.tgz", + "integrity": "sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw==", "requires": { "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", + "@babel/generator": "^7.6.3", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.2", - "@babel/types": "^7.6.0", + "@babel/parser": "^7.6.3", + "@babel/types": "^7.6.3", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" @@ -119,9 +119,9 @@ } }, "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.3.tgz", + "integrity": "sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -4540,9 +4540,9 @@ } }, "promise-android-tools": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.2.tgz", - "integrity": "sha512-14jRUvTB7BvCuLp+XE7VNKJtmbQ4a2hbbkpb+WwfI7wIWdkBDbfb1cWbeZjbqMaA6bTW+AfJLLrAzHJGfwp0Jg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.3.tgz", + "integrity": "sha512-8usNEZkE4pHw2efMfsiPe7MbgcBDdVQjfbA3w1BC2otNiTq8WtgY/YYlRAjzceFJyMsD5H13yYr97nyigOa9ig==", "requires": { "chai": "^4.1.2", "chai-as-promised": "^7.1.1", diff --git a/package.json b/package.json index 5dbcdb66..30a0f8d9 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "jquery": "^3.1.1", "jquery-i18next": "^1.2.0", "mkdirp": "^0.5.1", - "promise-android-tools": "^1.0.2", + "promise-android-tools": "^1.0.3", "request": "^2.79.0", "system-image-node-module": "^1.0.9", "winston": "^2.3.1" diff --git a/src/devices.js b/src/devices.js index 3bce3e48..44b0d8f7 100644 --- a/src/devices.js +++ b/src/devices.js @@ -139,7 +139,7 @@ function install(steps) { global.mainEvent.emit("user:write:speed", 0); resolve(); }, 1000); - }); + }).catch(reject); }); }); break; @@ -209,10 +209,7 @@ function install(steps) { installPromises.push(() => { return new Promise(function(resolve, reject) { utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "System-Image"); - global.mainEvent.emit("user:write:under", "Going through system-image process"); - systemImage.installLatestVersion(Object.assign({device: "hammerhead"}), global.installProperties.settings).then(() => { + systemImage.installLatestVersion(global.installProperties.settings).then(() => { utils.log.debug(step.type + " done"); resolve(); }).catch(e => errorToUser(e, "systemimage")); diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index d97f3a48..559676c9 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -110,7 +110,7 @@ script. views.show("select-os"); }); - ipcRenderer.on("user:configure", (event, output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { + ipcRenderer.on("user:configure", (osInstructs) => { views.show("install"); $("#options-channel").append(channels); options.optionsValToUser(); @@ -145,11 +145,11 @@ script. views.show("working", "particles"); ipcRenderer.on("device:wait:device-selects-ready", (event, deviceSelects) => { + footer.topText.set("Waiting for device", true); + footer.underText.set("Please connect your device with a USB cable"); if (!remote.getGlobal("installProperties").device) { // if the device is not yet set (i.e. from a cli argument), prompt the user $("#device-select").append(deviceSelects); - footer.topText.set("Waiting for device", true); - footer.underText.set("Please connect your device with a USB cable"); views.show("wait-for-device"); // Button to open device selector $("#btn-modal-select-device").click(() => { diff --git a/src/main.js b/src/main.js index 12c35a45..0ea0fd6b 100755 --- a/src/main.js +++ b/src/main.js @@ -127,7 +127,16 @@ ipcMain.on("createBugReport", (event, title) => { ipcMain.on("device:selected", (event, device) => { adb.stopWaiting(); global.installProperties.device = device; - mainWindow.webContents.send("user:os", global.installConfig, devices.getOsSelects(global.installConfig.operating_systems)); + if(devices.getOsSelects(global.installConfig.operating_systems).length > 1) { + // ask for os selection if there's one os + mainWindow.webContents.send( + "user:os", + global.installConfig, devices.getOsSelects(global.installConfig.operating_systems) + ); + } else { + // immediately jump to configure if there's only one os + devices.install(global.installConfig.operating_systems[0].steps); + } }); // The user selected an os @@ -231,9 +240,8 @@ mainEvent.on("user:device-unsupported", (device) => { }); // Set the install configuration data -mainEvent.on("user:configure", (output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid) => { - global.installProperties.device = device; - if (mainWindow) mainWindow.webContents.send("user:configure", output, device, channels, ubuntuCom, autoDetected, isLegacyAndroid); +mainEvent.on("user:configure", (osInstructs) => { + if (mainWindow) mainWindow.webContents.send("user:configure", osInstructs); }); // The user selected a device @@ -262,8 +270,10 @@ function createWindow () { // Tasks we need for every start mainWindow.webContents.on("did-finish-load", () => { adb.startServer().then(() => { - devices.waitForDevice(); - }).catch(e => utils.errorToUser("Failed to start adb server: " + e)); + if (!global.installProperties.device) { + devices.waitForDevice(); + } + }).catch(e => utils.errorToUser(e, "Failed to start adb server")); api.getDeviceSelects().then((out) => { mainWindow.webContents.send("device:wait:device-selects-ready", out); }).catch(e => { diff --git a/src/system-image.js b/src/system-image.js index f5da6f4f..50e9fd4f 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -49,11 +49,11 @@ var installLatestVersion = (options) => { global.mainEvent.emit("user:write:progress", progress*100); }).then(() => { resolve(); - }).catch(e => utils.errorToUser("Push failed: Failed push: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed to create target dir: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed to mount: " + e)); - }).catch(e => utils.errorToUser("Push failed: Failed wipe cache: " + e)); - }); + }).catch(e => utils.errorToUser(e, "Push failed: Failed push")); + }).catch(e => utils.errorToUser(e, "Push failed: Failed to create target dir")); + }).catch(e => utils.errorToUser(e, "Push failed: Failed to mount")); + }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); + }).catch(e => utils.errorToUser(e, "Download failed")); }); } diff --git a/src/utils.js b/src/utils.js index a7be29b3..d77d2b43 100644 --- a/src/utils.js +++ b/src/utils.js @@ -127,7 +127,7 @@ function getUpdateAvailable() { getLatestInstallerVersion().then((latestVersion) => { if(latestVersion != global.packageInfo.version) resolve(); else reject(); - }); + }).catch(resolve); }); } @@ -209,7 +209,7 @@ function checksumFile(file) { return new Promise(function(resolve, reject) { fs.access(path.join(file.path, path.basename(file.url)), (err) => { if (err) { - reject(); + reject(err); } else { if (!file.checksum) { // No checksum so return true; @@ -221,7 +221,7 @@ function checksumFile(file) { }, function(err, sum) { utils.log.debug("checked: " +path.basename(file.url), sum === file.checksum); if (sum === file.checksum) resolve(); - else reject(); + else reject("checksum mismatch: calculated " + sum + " but expected " + file.checksum); }); } } From f4fa81c3dd09a5149298dfe8f75314d9d7b89d2b Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 17 Oct 2019 01:23:19 +0200 Subject: [PATCH 12/53] Configure only if there's something to configure! --- src/main.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 0ea0fd6b..b6820f10 100755 --- a/src/main.js +++ b/src/main.js @@ -241,7 +241,15 @@ mainEvent.on("user:device-unsupported", (device) => { // Set the install configuration data mainEvent.on("user:configure", (osInstructs) => { - if (mainWindow) mainWindow.webContents.send("user:configure", osInstructs); + if(osInstructs.options) { + // If there's something to configure, configure it! + if (mainWindow) { + mainWindow.webContents.send("user:configure", osInstructs); + } + } else { + // If there's nothing to configure, don't configure anything + devices.install(osInstructs.steps); + } }); // The user selected a device From 0fa62f8a425cf1fe892122594351e3f1eaa84b73 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 17 Oct 2019 01:28:33 +0200 Subject: [PATCH 13/53] Actually configure --- src/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index b6820f10..1c1c0228 100755 --- a/src/main.js +++ b/src/main.js @@ -135,7 +135,7 @@ ipcMain.on("device:selected", (event, device) => { ); } else { // immediately jump to configure if there's only one os - devices.install(global.installConfig.operating_systems[0].steps); + mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); } }); @@ -143,7 +143,7 @@ ipcMain.on("device:selected", (event, device) => { ipcMain.on("os:selected", (event, osIndex) => { global.installProperties.osIndex = osIndex; utils.log.debug(global.installConfig.operating_systems[osIndex]); - devices.install(global.installConfig.operating_systems[osIndex].steps); + mainEvent.emit("user:configure", global.installConfig.operating_systems[osIndex]); }); //============================================================================== From b1ffabe494d1806d222edabcbb82c4d5806ae2a0 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Thu, 17 Oct 2019 02:08:47 +0200 Subject: [PATCH 14/53] Generate options --- src/html/index.pug | 1 - src/html/modals/options.pug | 2 +- src/html/scripts/main.pug | 40 ++++---------------- src/html/scripts/ui.pug | 75 +------------------------------------ src/html/views/install.pug | 23 ------------ 5 files changed, 10 insertions(+), 131 deletions(-) delete mode 100644 src/html/views/install.pug diff --git a/src/html/index.pug b/src/html/index.pug index f64c4bfd..e7c1caf3 100644 --- a/src/html/index.pug +++ b/src/html/index.pug @@ -18,7 +18,6 @@ html // Views include views/done include views/reboot - include views/install include views/select-os include views/not-supported include views/wait-for-device diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 105ec026..3ce2c1be 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -9,4 +9,4 @@ div form#options-form.form-horizontal .modal-footer - button#btn-options-close.btn.btn-default(type='button', data-dismiss='modal') Close + button#btn-options-close.btn.btn-default(type='button', data-dismiss='modal') Next diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 559676c9..23482225 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -100,46 +100,22 @@ script. footer.underText.set("The device " + device + " is not supported"); $("[id=your-device]").text(device); views.show("not-supported"); - $("#btn-inst").hide(); }); ipcRenderer.on("user:os", (event, installConfig, osSelects) => { global.installConfig = installConfig; global.installConfig.os_to_install = undefined; $("#options-os").append(osSelects); - views.show("select-os"); - }); - - ipcRenderer.on("user:configure", (osInstructs) => { - views.show("install"); - $("#options-channel").append(channels); - options.optionsValToUser(); - modals.hide('select-device'); - $("#btn-inst").show(); - footer.topText.set(output.name); - if (isLegacyAndroid && !ubuntuCom) { - $("#legacy-android-note").show(); - modals.show("legacy-android"); - } - if (autoDetected) { - footer.underText.set("Are you ready for Ubuntu Touch on your " + output.name + "?"); - } else { - footer.underText.set("Please connect your " + output.name + " with a USB cable."); - } - $("#your-ubp-device").text(output.name+" ("+output.device+")"); + $("#your-ubp-device").text(installConfig.name+" ("+installConfig.codename+")"); $("#your-ubp-device").click(() => { - shell.openExternal("https://devices.ubuntu-touch.io/device/"+output.device); + shell.openExternal("https://devices.ubuntu-touch.io/device/" + installConfig.codename); }); + views.show("select-os"); + }); - $("#btn-installModal").click(() => { - views.show("working", "particles"); - $("#progress").show(); - ipcRenderer.send("install", { - device: output.device, - channel: options.get("channel", true), - wipe: options.get("wipe", true) - }); - }); + ipcRenderer.on("user:configure", (event, osInstructs) => { + modals.show("options"); + osInstructs.options.forEach(addOption); }); views.show("working", "particles"); @@ -168,5 +144,3 @@ script. // Assign buttons modals.bind("options"); - // Initialize options modal - options.init(); diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index a360855a..12516647 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -1,13 +1,5 @@ script. - const optionsVal = [ - {id: "channel", type: "select"}, - {id: "wipe", type: "checkbox", note: true}, - {id: "custom-tools", type: "checkbox"}, - {id: "custom-tools-adb", type: "input"}, - {id: "custom-tools-fastboot", type: "input"} - ]; - const switchHide = (from, to) => { $("."+from).hide(); $("."+to).show(); @@ -126,7 +118,7 @@ script. bind: modal => { $("#btn-modal-"+modal).click(() => { $('#'+modal+'-modal').modal('show'); - }) + }); }, show: modal => { $('#'+modal+'-modal').modal('show'); @@ -160,60 +152,6 @@ script. $("#options-form").append(_div); } - const getOptionVal = (option, raw) => { - var ret = false; - optionsVal.forEach((optionVal) => { - if (optionVal.id === option) { - switch (optionVal.type) { - case "select": - ret = raw ? $("#options-" + optionVal.id).find(":selected").val() : - $("#options-" + optionVal.id).find(":selected").text(); - break; - case "checkbox": - ret = $("#options-" + optionVal.id).is(':checked') - if (!raw) ret = ret ? "Yes" : "No"; - break; - default: - ret = $("#options-" + optionVal.id).val() - break; - } - } - }); - return ret; - } - - const optionsValToUser = () => { - optionsVal.forEach(option => { - userText.set(option.id, getOptionVal(option.id)); - }); - } - - const options = { - get: getOptionVal, - init: () => { - $("#btn-options-close").click(() => { - optionsValToUser(); - }); - $("#options-wipe").change(() => { - if (getOptionVal("wipe", true)) { - $("#options-wipe-note").show(); - switchHide("modal-install-body-no-wipe", "modal-install-body-wipe"); - } else { - $("#options-wipe-note").hide(); - switchHide("modal-install-body-wipe", "modal-install-body-no-wipe"); - } - }); - // $("#options-custom-tools").change(() => { - // if (getOptionVal("custom-tools", true)) { - // $("#options-custom-tools-area").show(); - // } else { - // $("#options-custom-tools-area").hide(); - // } - // }); - }, - optionsValToUser: optionsValToUser - } - $("#btn-bugreport").click(() => { var title = $("#error-body").text(); ipcRenderer.send("createBugReport", title); @@ -240,19 +178,10 @@ script. modals.show('developer-mode-info'); }); - $("#btn-inst").click(() => { + $("#btn-options-close").click(() => { modals.show('install'); - // if(options.get("custom-tools", true)) { - // utils.setCustomPlatformTool("adb", options.get("custom-tools-adb")); - // utils.setCustomPlatformTool("fastboot", options.get("custom-tools-fastboot")); - // } }); - // if(global.packageInfo.isSnap) { - // $("#options-custom-tools").prop("disabled", true); - // $("#options-custom-tools-snap-note").show(); - // } - if (process.platform === "win32" && !localStorage.getItem('neverAskForWindowsDrivers')) { modals.show('windows-drivers'); } diff --git a/src/html/views/install.pug b/src/html/views/install.pug deleted file mode 100644 index 816a0c8e..00000000 --- a/src/html/views/install.pug +++ /dev/null @@ -1,23 +0,0 @@ -#views-install.main.container.views(hidden='hidden') - .row - .col-xs-6 - img(style='height: 350px; margin: auto; display: block;', src='../screens/Screen6.jpg') - .col-xs-6(style='height: 100%') - h4.user-install-header(style='font-weight: bold;') Ready to install! - p - | Your device: #[a#your-ubp-device] - #switch-note(hidden='hidden') - p This device is running old Canonical images! - #legacy-android-note(hidden='hidden') - p If your device is running Android, you might have to #[a(onclick="shell.openExternal('https://docs.ubports.com/en/latest/userguide/install.html#install-on-legacy-android-devices')") install the old Ubuntu Touch version from Canonical] first to unlock the bootloader. - h3 - small Install options - table.table - tr - td Channel - td.user-channel - tr - td Wipe - td.user-wipe - button#btn-modal-options.btn.btn-default(type='button', style='width: 100%;', hidden='') Change options - button#btn-inst.btn.btn-primary(type='button', style='width: 100%; margin-top: 10px;', hidden='') Install From 2f989d0c08a3f4c80443873a1ada17ce9f3e5fc5 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 20:38:23 +0200 Subject: [PATCH 15/53] read options values --- src/html/scripts/ui.pug | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index 12516647..edada154 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -133,7 +133,7 @@ script. _div.className = "form-group"; let _label = document.createElement("label"); _label.className = "col-xs-3 control-label"; - _label.appendChild(document.createTextNode(option.label)); + _label.appendChild(document.createTextNode(option.name)); _div.appendChild(_label); let _subdiv = document.createElement("div"); @@ -143,13 +143,17 @@ script. _input.className = "form-control space"; _input.type = option.type; _input.value = option.value; - _input.id = "options-" + option.id; + _input.id = "options-" + option.var; console.log(_input); console.log(_div); _subdiv.appendChild(_input); _div.appendChild(_subdiv); $("#options-form").append(_div); + $("#btn-options-close").click(() => { + if (option.type != "checkbox") console.log(option.var + ": " + $("#options-" + option.var).val()); + else console.log(option.var + ": " + $("#options-" + option.var).is(":checked")); + }); } $("#btn-bugreport").click(() => { From 62e6d8cc58b884fcfeec98b2f7504ee03205d8bd Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 20:50:25 +0200 Subject: [PATCH 16/53] set generated option vars --- src/html/scripts/ui.pug | 4 ++-- src/main.js | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index edada154..f160fe93 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -151,8 +151,8 @@ script. _div.appendChild(_subdiv); $("#options-form").append(_div); $("#btn-options-close").click(() => { - if (option.type != "checkbox") console.log(option.var + ": " + $("#options-" + option.var).val()); - else console.log(option.var + ": " + $("#options-" + option.var).is(":checked")); + if (option.type != "checkbox") ipcRenderer.send("option", option.var, $("#options-" + option.var).val()); + else ipcRenderer.send("option", option.var, $("#options-" + option.var).is(":checked")); }); } diff --git a/src/main.js b/src/main.js index 1c1c0228..6f4d0d50 100755 --- a/src/main.js +++ b/src/main.js @@ -88,7 +88,7 @@ if (cli.file) { global.installProperties = { device: global.installConfig ? global.installConfig.codename : cli.device, cli: cli.cli, - settings: cli.settings ? JSON.parse(cli.settings) : undefined, + settings: cli.settings ? JSON.parse(cli.settings) : {}, verbose: (cli.verbose || cli.debug), debug: cli.debug }; @@ -146,6 +146,11 @@ ipcMain.on("os:selected", (event, osIndex) => { mainEvent.emit("user:configure", global.installConfig.operating_systems[osIndex]); }); +// The user selected an os +ipcMain.on("option", (event, targetVar, value) => { + global.installProperties.settings[targetVar] = value; +}); + //============================================================================== // RENDERER COMMUNICATION //============================================================================== From cce3c61fb00bdf74a9d32ae0b199b81c3d49704d Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 21:26:57 +0200 Subject: [PATCH 17/53] Properly create select options --- src/html/scripts/ui.pug | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index f160fe93..6719827f 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -139,14 +139,21 @@ script. let _subdiv = document.createElement("div"); _subdiv.className = "col-xs-9"; - let _input = document.createElement("input"); + let _input = document.createElement(option.type == "select" ? "select" : "input"); _input.className = "form-control space"; - _input.type = option.type; + if (option.type != "select") _input.type = option.type; _input.value = option.value; _input.id = "options-" + option.var; - console.log(_input); - console.log(_div); + if (option.type == "select") { + for (var i = 0; i < option.values.length; i++) { + let _value = document.createElement("option"); + _value.value = option.values[i].value; + _value.appendChild(document.createTextNode(option.values[i].label)) + _input.appendChild(_value) + } + } + _subdiv.appendChild(_input); _div.appendChild(_subdiv); $("#options-form").append(_div); From 4f6e116aa7b67f8d81e748b28d05da07c364500e Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 21:35:23 +0200 Subject: [PATCH 18/53] Add comments and improve code quality of addOption() --- src/html/scripts/ui.pug | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index 6719827f..63eb647a 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -129,37 +129,49 @@ script. } function addOption(option) { + // div to contain entire option row let _div = document.createElement("div"); _div.className = "form-group"; + + // label describing the option let _label = document.createElement("label"); _label.className = "col-xs-3 control-label"; _label.appendChild(document.createTextNode(option.name)); _div.appendChild(_label); + // div to contain input/select element let _subdiv = document.createElement("div"); _subdiv.className = "col-xs-9"; + // create the actual input element let _input = document.createElement(option.type == "select" ? "select" : "input"); - _input.className = "form-control space"; + if (option.type != "checkbox") _input.className = "form-control space"; if (option.type != "select") _input.type = option.type; _input.value = option.value; _input.id = "options-" + option.var; + // if it's a select, add the values if (option.type == "select") { for (var i = 0; i < option.values.length; i++) { let _value = document.createElement("option"); _value.value = option.values[i].value; - _value.appendChild(document.createTextNode(option.values[i].label)) - _input.appendChild(_value) + _value.appendChild(document.createTextNode(option.values[i].label)); + _input.appendChild(_value); } } + // put it all in there _subdiv.appendChild(_input); _div.appendChild(_subdiv); $("#options-form").append(_div); + + // send data for this option to main for processing $("#btn-options-close").click(() => { - if (option.type != "checkbox") ipcRenderer.send("option", option.var, $("#options-" + option.var).val()); - else ipcRenderer.send("option", option.var, $("#options-" + option.var).is(":checked")); + if (option.type == "checkbox") { + ipcRenderer.send("option", option.var, $("#options-" + option.var).is(":checked")); + } else { + ipcRenderer.send("option", option.var, $("#options-" + option.var).val()); + } }); } From 2d5955b22a1a0c8a0ec1e862e74e4640716e45cb Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 22:34:14 +0200 Subject: [PATCH 19/53] Communicate settings to install process --- src/html/modals/install.pug | 4 ++-- src/main.js | 6 +++++- src/utils.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/html/modals/install.pug b/src/html/modals/install.pug index 8759a5ee..22d63c2b 100644 --- a/src/html/modals/install.pug +++ b/src/html/modals/install.pug @@ -27,5 +27,5 @@ b NOTE | You have chosen to wipe, this will erase all your data. .modal-footer - button.btn.btn-default(type='button', data-dismiss='modal') Cancel - button#btn-installModal.btn.btn-primary(type='button', data-dismiss='modal') Continue + button.btn.btn-default(type='button', data-dismiss='modal', onclick="ipcRenderer.send('restart');") Cancel + button#btn-installModal.btn.btn-primary(type='button', data-dismiss='modal', onclick="ipcRenderer.send('install');") Continue diff --git a/src/main.js b/src/main.js index 6f4d0d50..017dd962 100755 --- a/src/main.js +++ b/src/main.js @@ -116,6 +116,11 @@ ipcMain.on("error_ignored", () => { utils.log.debug("ERROR IGNORED"); }); +// Begin install process +ipcMain.on("install", () => { + devices.install(global.installConfig.operating_systems[global.installProperties.osIndex].steps); +}); + // Submit a bug-report ipcMain.on("createBugReport", (event, title) => { utils.createBugReport(title, global.installProperties, (body) => { @@ -179,7 +184,6 @@ mainEvent.on("user:low-power", () => { // Restart the installer mainEvent.on("restart", () => { global.installProperties.device = undefined; - global.installProperties.channel = undefined; utils.log.debug("WINDOW RELOADED"); mainWindow.reload(); }); diff --git a/src/utils.js b/src/utils.js index d77d2b43..5cfd3f4c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -75,7 +75,7 @@ function createBugReport(title, installProperties, callback) { callback("*Automatically generated error report* %0D%0A" + "UBports Installer Version: " + global.packageInfo.version + " %0D%0A" + "Device: " + (installProperties.device ? installProperties.device : "Not detected") + "%0D%0A" + - "Channel: " + (installProperties.channel ? installProperties.channel : "Not yet set") + "%0D%0A" + + "Channel: " + (installProperties.settings && installProperties.settings.channel ? installProperties.settings.channel : "Not yet set") + "%0D%0A" + "Package: " + (isSnap() ? "snap" : (packageInfo.package || "source")) + "%0D%0A" + "Operating System: " + getCleanOs() + " " + os.arch() + " %0D%0A" + "NodeJS version: " + process.version + " %0D%0A%0D%0A" + From e24091c39ae875e01364abb262d0d0b6fc9c69a7 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 22:40:05 +0200 Subject: [PATCH 20/53] State your business, traveller! --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 017dd962..7bfe0f41 100755 --- a/src/main.js +++ b/src/main.js @@ -347,6 +347,6 @@ app.on('activate', function () { process.on('unhandledRejection', (r) => { utils.log.error(r); - if (mainWindow) utils.errorToUser(r); + if (mainWindow) utils.errorToUser(r, "unhandledRejection"); else utils.die(r); }); From 8e95f3f1b2bf450262c4def5cf8600435eaebff0 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 23:00:00 +0200 Subject: [PATCH 21/53] Allow checkboxes to be checked by default. It's in their nature. Anything else would be inhumane. --- src/html/scripts/ui.pug | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index 63eb647a..e1381f81 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -148,6 +148,8 @@ script. if (option.type != "checkbox") _input.className = "form-control space"; if (option.type != "select") _input.type = option.type; _input.value = option.value; + // HACK: the checked property behaves super weird for some reason + if (option.type == "checkbox" && option.value) _input.checked = option.value; _input.id = "options-" + option.var; // if it's a select, add the values From c7297ae1c04c38c86cdaed983d9b94247a9f26c7 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Fri, 18 Oct 2019 23:19:03 +0200 Subject: [PATCH 22/53] Add support for step conditions --- src/devices.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/devices.js b/src/devices.js index 44b0d8f7..94351557 100644 --- a/src/devices.js +++ b/src/devices.js @@ -117,6 +117,10 @@ function addPathToFiles(files, device) { function install(steps) { var installPromises = []; steps.forEach((step) => { + if (step.condition && global.installProperties.settings[step.condition.var] != step.condition.value) { + // If the condition is not met, no need to do anything + return; + } switch (step.type) { case "download": installPromises.push(() => { From d84679f7c6774db427dedd8da9267de0b61e601d Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 00:02:40 +0200 Subject: [PATCH 23/53] Fix install as per current state --- src/devices.js | 2 +- src/system-image.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/devices.js b/src/devices.js index 94351557..16ffe69e 100644 --- a/src/devices.js +++ b/src/devices.js @@ -213,7 +213,7 @@ function install(steps) { installPromises.push(() => { return new Promise(function(resolve, reject) { utils.log.debug("step: " + JSON.stringify(step)); - systemImage.installLatestVersion(global.installProperties.settings).then(() => { + systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { utils.log.debug(step.type + " done"); resolve(); }).catch(e => errorToUser(e, "systemimage")); diff --git a/src/system-image.js b/src/system-image.js index 50e9fd4f..31de867e 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -42,7 +42,7 @@ var installLatestVersion = (options) => { mainEvent.emit("user:write:working", "push"); mainEvent.emit("user:write:status", "Sending"); mainEvent.emit("user:write:under", "Sending files to the device"); - adb.wipeCache().then(() => { + // adb.wipeCache().then(() => { adb.shell("mount -a").then(() => { adb.shell("mkdir -p /cache/recovery").then(() => { adb.pushArray(files, (progress) => { @@ -52,7 +52,7 @@ var installLatestVersion = (options) => { }).catch(e => utils.errorToUser(e, "Push failed: Failed push")); }).catch(e => utils.errorToUser(e, "Push failed: Failed to create target dir")); }).catch(e => utils.errorToUser(e, "Push failed: Failed to mount")); - }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); + // }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); }).catch(e => utils.errorToUser(e, "Download failed")); }); } From 98e209cab448b66bc525c12fef30e94b0aff5268 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 08:50:54 +0200 Subject: [PATCH 24/53] Move options-specific code to options.pug --- src/html/modals/options.pug | 54 +++++++++++++++++++++++++++++++++++++ src/html/scripts/main.pug | 5 ---- src/html/scripts/ui.pug | 49 --------------------------------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 3ce2c1be..94d18b1a 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -10,3 +10,57 @@ form#options-form.form-horizontal .modal-footer button#btn-options-close.btn.btn-default(type='button', data-dismiss='modal') Next + script. + function addOption(option) { + // div to contain entire option row + let _div = document.createElement("div"); + _div.className = "form-group"; + + // label describing the option + let _label = document.createElement("label"); + _label.className = "col-xs-3 control-label"; + _label.appendChild(document.createTextNode(option.name)); + _div.appendChild(_label); + + // div to contain input/select element + let _subdiv = document.createElement("div"); + _subdiv.className = "col-xs-9"; + + // create the actual input element + let _input = document.createElement(option.type == "select" ? "select" : "input"); + if (option.type != "checkbox") _input.className = "form-control space"; + if (option.type != "select") _input.type = option.type; + _input.value = option.value; + // HACK: the checked property behaves super weird for some reason + if (option.type == "checkbox" && option.value) _input.checked = option.value; + _input.id = "options-" + option.var; + + // if it's a select, add the values + if (option.type == "select") { + for (var i = 0; i < option.values.length; i++) { + let _value = document.createElement("option"); + _value.value = option.values[i].value; + _value.appendChild(document.createTextNode(option.values[i].label)); + _input.appendChild(_value); + } + } + + // put it all in there + _subdiv.appendChild(_input); + _div.appendChild(_subdiv); + $("#options-form").append(_div); + + // send data for this option to main for processing + $("#btn-options-close").click(() => { + if (option.type == "checkbox") { + ipcRenderer.send("option", option.var, $("#options-" + option.var).is(":checked")); + } else { + ipcRenderer.send("option", option.var, $("#options-" + option.var).val()); + } + }); + } + + ipcRenderer.on("user:configure", (event, osInstructs) => { + modals.show("options"); + osInstructs.options.forEach(addOption); + }); diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 23482225..7c4f358f 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -113,11 +113,6 @@ script. views.show("select-os"); }); - ipcRenderer.on("user:configure", (event, osInstructs) => { - modals.show("options"); - osInstructs.options.forEach(addOption); - }); - views.show("working", "particles"); ipcRenderer.on("device:wait:device-selects-ready", (event, deviceSelects) => { diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index e1381f81..c45c362a 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -128,55 +128,6 @@ script. } } - function addOption(option) { - // div to contain entire option row - let _div = document.createElement("div"); - _div.className = "form-group"; - - // label describing the option - let _label = document.createElement("label"); - _label.className = "col-xs-3 control-label"; - _label.appendChild(document.createTextNode(option.name)); - _div.appendChild(_label); - - // div to contain input/select element - let _subdiv = document.createElement("div"); - _subdiv.className = "col-xs-9"; - - // create the actual input element - let _input = document.createElement(option.type == "select" ? "select" : "input"); - if (option.type != "checkbox") _input.className = "form-control space"; - if (option.type != "select") _input.type = option.type; - _input.value = option.value; - // HACK: the checked property behaves super weird for some reason - if (option.type == "checkbox" && option.value) _input.checked = option.value; - _input.id = "options-" + option.var; - - // if it's a select, add the values - if (option.type == "select") { - for (var i = 0; i < option.values.length; i++) { - let _value = document.createElement("option"); - _value.value = option.values[i].value; - _value.appendChild(document.createTextNode(option.values[i].label)); - _input.appendChild(_value); - } - } - - // put it all in there - _subdiv.appendChild(_input); - _div.appendChild(_subdiv); - $("#options-form").append(_div); - - // send data for this option to main for processing - $("#btn-options-close").click(() => { - if (option.type == "checkbox") { - ipcRenderer.send("option", option.var, $("#options-" + option.var).is(":checked")); - } else { - ipcRenderer.send("option", option.var, $("#options-" + option.var).val()); - } - }); - } - $("#btn-bugreport").click(() => { var title = $("#error-body").text(); ipcRenderer.send("createBugReport", title); From 152635aba3219189a44eb77d04a542eddc234f58 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 09:04:58 +0200 Subject: [PATCH 25/53] Make sure not to add options to form twice --- src/html/modals/options.pug | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 94d18b1a..5e02d225 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -60,7 +60,11 @@ }); } + let optionsAdded = false; ipcRenderer.on("user:configure", (event, osInstructs) => { modals.show("options"); - osInstructs.options.forEach(addOption); + if (!optionsAdded) { + optionsAdded = true; + osInstructs.options.forEach(addOption); + } }); From 035f69586081e9f44294e68c15a87cc44473a23e Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 09:16:34 +0200 Subject: [PATCH 26/53] remove modals.bind --- src/html/scripts/main.pug | 7 ------- src/html/scripts/ui.pug | 13 ------------- src/html/views/not-supported.pug | 2 +- src/html/views/wait-for-device.pug | 4 ++-- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 7c4f358f..1bbf2359 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -122,10 +122,6 @@ script. // if the device is not yet set (i.e. from a cli argument), prompt the user $("#device-select").append(deviceSelects); views.show("wait-for-device"); - // Button to open device selector - $("#btn-modal-select-device").click(() => { - modals.show('select-device'); - }); // Button to confirm device selection $("#btn-select-device").click(() => { var device = $("#device-select").find(":selected").attr("name"); @@ -136,6 +132,3 @@ script. ipcRenderer.send("device:selected", remote.getGlobal("installProperties").device); } }); - - // Assign buttons - modals.bind("options"); diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index c45c362a..a66287d6 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -115,11 +115,6 @@ script. } const modals = { - bind: modal => { - $("#btn-modal-"+modal).click(() => { - $('#'+modal+'-modal').modal('show'); - }); - }, show: modal => { $('#'+modal+'-modal').modal('show'); }, @@ -146,14 +141,6 @@ script. shell.openExternal("https://ubports.com/donate"); }); - $("#btn-modal-select-device-unsupported").click(() => { - modals.show('select-device'); - }); - - $("#btn-modal-dev-mode").click(() => { - modals.show('developer-mode-info'); - }); - $("#btn-options-close").click(() => { modals.show('install'); }); diff --git a/src/html/views/not-supported.pug b/src/html/views/not-supported.pug index 7ca068da..eead2348 100644 --- a/src/html/views/not-supported.pug +++ b/src/html/views/not-supported.pug @@ -18,4 +18,4 @@ h4(style='font-weight: bold;') You want to try to install anyway? p | You can try selecting your device manually, but please only do so if you're sure that your exact model is actually supported! You might also want to #[a(onclick="shell.openExternal('http://devices.ubuntu-touch.io')") file a bug]. - button#btn-modal-select-device-unsupported.btn.btn-default(type='button', style='width: 100%;') Select device manually + button#btn-modal-select-device-unsupported.btn.btn-default(type='button', style='width: 100%;', onclick="modals.show('select-device');") Select device manually diff --git a/src/html/views/wait-for-device.pug b/src/html/views/wait-for-device.pug index 4988c83b..385d2753 100644 --- a/src/html/views/wait-for-device.pug +++ b/src/html/views/wait-for-device.pug @@ -6,6 +6,6 @@ h4(style='font-weight: bold;') Please connect your device p Welcome to the UBports Installer! This tool will walk you through the Ubuntu Touch installation process. Don't worry, it's easy! p Connect your device to the computer and enable developer mode. After that, your device should be detected automatically. - button#btn-modal-dev-mode.btn.btn-primary(type='button', style='width: 100%; margin-bottom: 10px;') How do I enable developer mode? + button#btn-modal-dev-mode.btn.btn-primary(type='button', style='width: 100%; margin-bottom: 10px;', onclick="modals.show('developer-mode-info');") How do I enable developer mode? p If your device is not detected automatically, you can select it manually to proceed. Please note that the UBports Installer will only work on #[a(onclick="shell.openExternal('http://devices.ubuntu-touch.io')") supported devices]. - button#btn-modal-select-device.btn.btn-default(type='button', style='width: 100%;') Select device manually + button#btn-modal-select-device.btn.btn-default(type='button', style='width: 100%;', onclick="modals.show('select-device');") Select device manually From 394189212d44798dc30ce73e4b783a07d0a789fe Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 09:19:40 +0200 Subject: [PATCH 27/53] Remove unneeded function --- src/html/scripts/ui.pug | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/html/scripts/ui.pug b/src/html/scripts/ui.pug index a66287d6..a289da3e 100644 --- a/src/html/scripts/ui.pug +++ b/src/html/scripts/ui.pug @@ -22,10 +22,6 @@ script. $("."+cat+"-"+id).text(text); } - const getValue = (cat, id) => { - return $("#"+cat+"-"+id).val(); - } - const animations = { hideAll: () => { $("#particles-foreground").hide(); From 6b52adb742ee441f631a8e32a55895c67fa73dcc Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 10:19:07 +0200 Subject: [PATCH 28/53] Add support for user_actions defined in config file --- src/html/index.pug | 1 + src/html/scripts/main.pug | 18 ++++++++++++++++++ src/html/views/user-action.pug | 8 ++++++++ src/img/phone_power_down.jpg | Bin 0 -> 119858 bytes src/img/phone_power_up.jpg | Bin 0 -> 110412 bytes src/main.js | 10 ++++++++++ 6 files changed, 37 insertions(+) create mode 100644 src/html/views/user-action.pug create mode 100644 src/img/phone_power_down.jpg create mode 100644 src/img/phone_power_up.jpg diff --git a/src/html/index.pug b/src/html/index.pug index e7c1caf3..ca6d338e 100644 --- a/src/html/index.pug +++ b/src/html/index.pug @@ -18,6 +18,7 @@ html // Views include views/done include views/reboot + include views/user-action include views/select-os include views/not-supported include views/wait-for-device diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 1bbf2359..5fea1ba0 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -113,6 +113,24 @@ script. views.show("select-os"); }); + ipcRenderer.on("user:action", (event, action) => { + views.show("user-action"); + $("#user-action-title").text(action.title) + $("#user-action-description").text(action.description) + + if (action.image) $("#user-action-image").attr("src", "../img/" + action.image + ".jpg"); + else $("#user-action-image").attr("src", "../screens/Screen6.jpg"); + + if (action.button) { + $("#user-action-button").one("click", () => { + event.sender.send("action:completed"); + }); + $("#user-action-button").show(); + } else { + $("#user-action-button").hide(); + } + }); + views.show("working", "particles"); ipcRenderer.on("device:wait:device-selects-ready", (event, deviceSelects) => { diff --git a/src/html/views/user-action.pug b/src/html/views/user-action.pug new file mode 100644 index 00000000..7406fd49 --- /dev/null +++ b/src/html/views/user-action.pug @@ -0,0 +1,8 @@ +#views-user-action.main.container.views(hidden='hidden') + .row + .col-xs-6 + img#user-action-image(style='height: 350px; margin: auto; display: block;', src='../screens/Screen6.jpg') + .col-xs-6 + h4#user-action-title(style='font-weight: bold;') + p#user-action-description + button#user-action-button.btn.btn-primary(hidden="false", type='button', style='width: 100%;') Continue diff --git a/src/img/phone_power_down.jpg b/src/img/phone_power_down.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac790336bc10ee13e25d9a2473badc7737c32eb1 GIT binary patch literal 119858 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

J6#ywO}fQT+|) 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�ewolfCmD!C{rpBXS{IWuvL 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 Q=n+ { }); }); +// Request user_action +mainEvent.on("user:action", (action, callback) => { + if (mainWindow) { + mainWindow.webContents.send("user:action", action); + if (action.button) { + ipcMain.on("action:completed", callback); + } + } +}); + // Prompt the user to reboot mainEvent.on("user:reboot", (i) => { if (mainWindow) mainWindow.webContents.send("user:reboot", i); From a0187f3eef67e2f9ac91366f7f69402eb612aace Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 10:34:40 +0200 Subject: [PATCH 29/53] Read user_action step from config file --- src/devices.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/devices.js b/src/devices.js index 16ffe69e..f8999bd6 100644 --- a/src/devices.js +++ b/src/devices.js @@ -234,6 +234,17 @@ function install(steps) { }); }); break; + case "user_action": + installPromises.push(() => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { + utils.log.debug(step.type + " done"); + resolve(); + }); + }); + }); + break; default: throw "error: unrecognized step type: " + step.type } From 4cd96b29d711d8449785b15018ca9d8336d25ddc Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 10:51:16 +0200 Subject: [PATCH 30/53] Instruct fallback reboots --- src/devices.js | 71 ++++++++------------------------------- src/html/index.pug | 1 - src/html/scripts/main.pug | 11 ------ src/html/views/reboot.pug | 27 --------------- src/main.js | 10 ------ 5 files changed, 14 insertions(+), 106 deletions(-) delete mode 100644 src/html/views/reboot.pug diff --git a/src/devices.js b/src/devices.js index f8999bd6..8e1002b8 100644 --- a/src/devices.js +++ b/src/devices.js @@ -42,59 +42,6 @@ var isLegacyAndroid = (device) => { } } -var instructReboot = (state, button, callback) => { - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Rebooting to " + state); - global.mainEvent.emit("user:write:under", "Waiting for device to enter " + state + " mode"); - var manualReboot = () => { - utils.log.info("Instructing manual reboot"); - utils.log.info(button[state]["instruction"]); - global.mainEvent.emit("user:reboot", { - button: button[state]["button"], - instruction: button[state]["instruction"], - state: state - }); - } - var rebootTimeout = setTimeout(() => { manualReboot(); }, 15000); - adb.hasAccess().then((hasAccess) => { - if (hasAccess) { - adb.reboot(state).then(() => { - clearTimeout(rebootTimeout); - global.mainEvent.emit("reboot:done"); - }).catch((err) => { - utils.log.warn("Adb failed to reboot!, " + err); - clearTimeout(rebootTimeout); - manualReboot(); - }); - } else { - clearTimeout(rebootTimeout); - manualReboot(); - } - if (state === "bootloader") { - fastboot.waitForDevice().then((err, errM) => { - if (err) { - utils.errorToUser(errM, "fastboot.waitForDevice"); - return; - } else { - clearTimeout(rebootTimeout); - global.mainEvent.emit("reboot:done"); - callback(); - return; - } - }); - } else { - adb.waitForDevice().then(() => { - clearTimeout(rebootTimeout); - global.mainEvent.emit("reboot:done"); - callback(); - return; - }); - } - }).catch((error) => { - utils.errorToUser(error, "Wait for device") - }); -} - function addPathToImages(images, device, group) { var ret = []; images.forEach((image) => { @@ -159,9 +106,10 @@ function install(steps) { resolve(); }).catch((error) => { utils.log.error("reboot failed: " + error); - // TODO instructReboot(step.to_state, undefined, resolve); - utils.log.info("REBOOT MANUALLY"); - resolve(); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { + utils.log.debug(step.type + " done"); + resolve(); + }); }); }); }); @@ -205,7 +153,16 @@ function install(steps) { fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { utils.log.debug(step.type + " done"); resolve(); - }).catch(e => errorToUser(e, "fastboot boot")); + }).catch((e) => { + if (step.fallback_user_action) { + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { + utils.log.debug(step.type + " done"); + resolve(); + }); + } else { + errorToUser(e, "fastboot boot"); + } + }); }); }); break; diff --git a/src/html/index.pug b/src/html/index.pug index ca6d338e..726447b3 100644 --- a/src/html/index.pug +++ b/src/html/index.pug @@ -17,7 +17,6 @@ html i.is-under // Views include views/done - include views/reboot include views/user-action include views/select-os include views/not-supported diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 5fea1ba0..8e7adb3b 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -51,17 +51,6 @@ script. }); }); - ipcRenderer.on("user:reboot", (e, i) => { - views.show("reboot-" + i.button); - $("[id=reboot-to-state]").text(i.state); - $("#complex-reboot-instruction").text(i.instruction); - }); - - ipcRenderer.on("reboot:done", () => { - $('#views-reboot-up').hide(); - $('#views-reboot-down').hide(); - }); - ipcRenderer.on("user:no-network", () => { modals.show('no-network'); }); diff --git a/src/html/views/reboot.pug b/src/html/views/reboot.pug deleted file mode 100644 index 27e0d05f..00000000 --- a/src/html/views/reboot.pug +++ /dev/null @@ -1,27 +0,0 @@ -#views-reboot-up.main.container.views(hidden='hidden') - .row - .col-xs-6 - img(style='height: 350px; margin: auto; display: block;', src='../screens/Screen2.jpg') - .col-xs-6 - h4(style='font-weight: bold;') - | Please reboot to - b#reboot-to-state bootloader - p - | The device needs to be in - b#reboot-to-state bootloader - | mode in order to continue. - p#complex-reboot-instruction -#views-reboot-down.main.container.views(hidden='hidden') - .row - .col-xs-7 - img(style='height: 350px; margin: auto; display: block;', src='../screens/Screen3.jpg') - .col-xs-5 - h4(style='font-weight: bold;') - | Please reboot to - b#reboot-to-state bootloader - p - | The device needs to be in - b#reboot-to-state bootloader - | mode in order to continue. - p#complex-reboot-instruction - diff --git a/src/main.js b/src/main.js index c336e0bd..9d23f4b2 100755 --- a/src/main.js +++ b/src/main.js @@ -210,16 +210,6 @@ mainEvent.on("user:action", (action, callback) => { } }); -// Prompt the user to reboot -mainEvent.on("user:reboot", (i) => { - if (mainWindow) mainWindow.webContents.send("user:reboot", i); -}); - -// Reboot complete, hide the reboot prompt -mainEvent.on("reboot:done", () => { - if (mainWindow) mainWindow.webContents.send("reboot:done"); -}); - // Control the progress bar mainEvent.on("user:write:progress", (progress) => { if (mainWindow) mainWindow.webContents.send("user:write:progress", progress); From 7a5b64d203ec35505c39e28858f6815de01b1f98 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 10:52:47 +0200 Subject: [PATCH 31/53] Wipe cache from adb --- src/system-image.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system-image.js b/src/system-image.js index 31de867e..50e9fd4f 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -42,7 +42,7 @@ var installLatestVersion = (options) => { mainEvent.emit("user:write:working", "push"); mainEvent.emit("user:write:status", "Sending"); mainEvent.emit("user:write:under", "Sending files to the device"); - // adb.wipeCache().then(() => { + adb.wipeCache().then(() => { adb.shell("mount -a").then(() => { adb.shell("mkdir -p /cache/recovery").then(() => { adb.pushArray(files, (progress) => { @@ -52,7 +52,7 @@ var installLatestVersion = (options) => { }).catch(e => utils.errorToUser(e, "Push failed: Failed push")); }).catch(e => utils.errorToUser(e, "Push failed: Failed to create target dir")); }).catch(e => utils.errorToUser(e, "Push failed: Failed to mount")); - // }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); + }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); }).catch(e => utils.errorToUser(e, "Download failed")); }); } From 134eb38bcc3bf2b95b4405bd5e0cbd0b14e2a81f Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 10:58:59 +0200 Subject: [PATCH 32/53] Hide select device prompt on detection --- src/html/scripts/main.pug | 1 + 1 file changed, 1 insertion(+) diff --git a/src/html/scripts/main.pug b/src/html/scripts/main.pug index 8e7adb3b..b353fed3 100644 --- a/src/html/scripts/main.pug +++ b/src/html/scripts/main.pug @@ -99,6 +99,7 @@ script. $("#your-ubp-device").click(() => { shell.openExternal("https://devices.ubuntu-touch.io/device/" + installConfig.codename); }); + modals.hide("select-device"); views.show("select-os"); }); From 58874b2c1664e42f004331e4588466cfd12b293c Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 11:01:23 +0200 Subject: [PATCH 33/53] Remove isLegacyAndroid \o/ --- src/devices.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/devices.js b/src/devices.js index 8e1002b8..7a043882 100644 --- a/src/devices.js +++ b/src/devices.js @@ -25,23 +25,6 @@ const path = require("path"); const downloadPath = utils.getUbuntuTouchDir(); -// HACK: This should be handled by the server, not locally -var isLegacyAndroid = (device) => { - switch (device) { - case "krillin": - case "vegetahd": - case "cooler": - case "frieza": - case "turbo": - case "arale": - utils.log.info("This is a legacy android device"); - return true; - default: - utils.log.debug("This is NOT a legacy android device"); - return false; - } -} - function addPathToImages(images, device, group) { var ret = []; images.forEach((image) => { From ff39ac35e12180019d5f2362c20c0d68d343252a Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 11:15:14 +0200 Subject: [PATCH 34/53] Move install promise assembly to its own function --- src/devices.js | 264 +++++++++++++++++++++++++------------------------ 1 file changed, 134 insertions(+), 130 deletions(-) diff --git a/src/devices.js b/src/devices.js index 7a043882..e73ac813 100644 --- a/src/devices.js +++ b/src/devices.js @@ -44,150 +44,154 @@ function addPathToFiles(files, device) { return ret; } -function install(steps) { - var installPromises = []; - steps.forEach((step) => { - if (step.condition && global.installProperties.settings[step.condition.var] != step.condition.value) { - // If the condition is not met, no need to do anything - return; - } - switch (step.type) { - case "download": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "download"); - global.mainEvent.emit("user:write:status", "Downloading " + step.group); - global.mainEvent.emit("user:write:under", "Downloading"); - utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { - global.mainEvent.emit("user:write:progress", progress*100); - global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); - }, (current, total) => { - utils.log.info("Downloaded file " + current + " of " + total); - }).then(() => { - utils.log.debug(step.type + " done"); - setTimeout(() => { - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:under", "Verifying download"); - global.mainEvent.emit("user:write:progress", 0); - global.mainEvent.emit("user:write:speed", 0); - resolve(); - }, 1000); - }).catch(reject); - }); +function installStep(step) { + if (step.condition && global.installProperties.settings[step.condition.var] != step.condition.value) { + // If the condition is not met, no need to do anything + return; + } + switch (step.type) { + case "download": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "download"); + global.mainEvent.emit("user:write:status", "Downloading " + step.group); + global.mainEvent.emit("user:write:under", "Downloading"); + utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { + global.mainEvent.emit("user:write:progress", progress*100); + global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); + }, (current, total) => { + utils.log.info("Downloaded file " + current + " of " + total); + }).then(() => { + utils.log.debug(step.type + " done"); + setTimeout(() => { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:under", "Verifying download"); + global.mainEvent.emit("user:write:progress", 0); + global.mainEvent.emit("user:write:speed", 0); + resolve(); + }, 1000); + }).catch(reject); }); - break; - case "adb:reboot": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles");+ - global.mainEvent.emit("user:write:status", "Rebooting"); - global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); - adb.reboot(step.to_state).then(() => { + }; + break; + case "adb:reboot": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); + adb.reboot(step.to_state).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch((error) => { + utils.log.error("reboot failed: " + error); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { utils.log.debug(step.type + " done"); resolve(); - }).catch((error) => { - utils.log.error("reboot failed: " + error); - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); }); }); }); - break; - case "fastboot:flash": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Flashing firmware"); - global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); - utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) - fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot flash")); - }); + }; + break; + case "fastboot:flash": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Flashing firmware"); + global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); + utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) + fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch(e => errorToUser(e, "fastboot flash")); }); - break; - case "fastboot:erase": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles");+ - global.mainEvent.emit("user:write:status", "Ceaning up"); - global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); - fastboot.erase(step.partition).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot erase")); - }); + }; + break; + case "fastboot:erase": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Ceaning up"); + global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); + fastboot.erase(step.partition).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch(e => errorToUser(e, "fastboot erase")); }); - break; - case "fastboot:boot": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Rebooting"); - global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); - fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch((e) => { - if (step.fallback_user_action) { - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); - } else { - errorToUser(e, "fastboot boot"); - } - }); + }; + break; + case "fastboot:boot": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); + fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch((e) => { + if (step.fallback_user_action) { + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { + utils.log.debug(step.type + " done"); + resolve(); + }); + } else { + errorToUser(e, "fastboot boot"); + } }); }); - break; - case "systemimage": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "systemimage")); - }); + }; + break; + case "systemimage": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch(e => errorToUser(e, "systemimage")); }); - break; - case "fastboot:update": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Updating system"); - global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); - fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot update")); - }); + }; + break; + case "fastboot:update": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Updating system"); + global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); + fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { + utils.log.debug(step.type + " done"); + resolve(); + }).catch(e => errorToUser(e, "fastboot update")); }); - break; - case "user_action": - installPromises.push(() => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); + }; + break; + case "user_action": + return () => { + return new Promise(function(resolve, reject) { + utils.log.debug("step: " + JSON.stringify(step)); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { + utils.log.debug(step.type + " done"); + resolve(); }); }); - break; - default: - throw "error: unrecognized step type: " + step.type - } + }; + break; + default: + throw "error: unrecognized step type: " + step.type + } +} + +function install(steps) { + var installPromises = []; + steps.forEach((step) => { + installPromises.push(installStep(step)); }); installPromises.push(() => { global.mainEvent.emit("user:write:done"); From bf0ac3cd02b2320f63f6f9bebe8daa11899de9c4 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 11:51:43 +0200 Subject: [PATCH 35/53] generic error handling on install promises --- src/devices.js | 237 +++++++++++++++++++++++-------------------------- 1 file changed, 113 insertions(+), 124 deletions(-) diff --git a/src/devices.js b/src/devices.js index e73ac813..05df2f93 100644 --- a/src/devices.js +++ b/src/devices.js @@ -45,165 +45,147 @@ function addPathToFiles(files, device) { } function installStep(step) { - if (step.condition && global.installProperties.settings[step.condition.var] != step.condition.value) { - // If the condition is not met, no need to do anything - return; - } switch (step.type) { case "download": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "download"); - global.mainEvent.emit("user:write:status", "Downloading " + step.group); - global.mainEvent.emit("user:write:under", "Downloading"); - utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { - global.mainEvent.emit("user:write:progress", progress*100); - global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); - }, (current, total) => { - utils.log.info("Downloaded file " + current + " of " + total); - }).then(() => { - utils.log.debug(step.type + " done"); - setTimeout(() => { - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:under", "Verifying download"); - global.mainEvent.emit("user:write:progress", 0); - global.mainEvent.emit("user:write:speed", 0); - resolve(); - }, 1000); - }).catch(reject); - }); - }; + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "download"); + global.mainEvent.emit("user:write:status", "Downloading " + step.group); + global.mainEvent.emit("user:write:under", "Downloading"); + utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { + global.mainEvent.emit("user:write:progress", progress*100); + global.mainEvent.emit("user:write:speed", Math.round(speed*100)/100); + }, (current, total) => { + utils.log.info("Downloaded file " + current + " of " + total); + }).then(() => { + setTimeout(() => { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:under", "Verifying download"); + global.mainEvent.emit("user:write:progress", 0); + global.mainEvent.emit("user:write:speed", 0); + resolve(); + }, 1000); + }).catch(reject); + }); break; case "adb:reboot": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles");+ - global.mainEvent.emit("user:write:status", "Rebooting"); - global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); - adb.reboot(step.to_state).then(() => { - utils.log.debug(step.type + " done"); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); + adb.reboot(step.to_state).then(() => { + resolve(); + }).catch((error) => { + utils.log.error("reboot failed: " + error); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { resolve(); - }).catch((error) => { - utils.log.error("reboot failed: " + error); - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); }); }); - }; + }); break; case "fastboot:flash": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Flashing firmware"); - global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); - utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) - fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot flash")); - }); - }; + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Flashing firmware"); + global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); + utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) + fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { + resolve(); + }).catch(e => errorToUser(e, "fastboot flash")); + }); break; case "fastboot:erase": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles");+ - global.mainEvent.emit("user:write:status", "Ceaning up"); - global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); - fastboot.erase(step.partition).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot erase")); - }); - }; + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:status", "Ceaning up"); + global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); + fastboot.erase(step.partition).then(() => { + resolve(); + }).catch(e => errorToUser(e, "fastboot erase")); + }); break; case "fastboot:boot": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Rebooting"); - global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); - fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch((e) => { - if (step.fallback_user_action) { - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); - } else { - errorToUser(e, "fastboot boot"); - } - }); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); + fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { + resolve(); + }).catch((e) => { + if (step.fallback_user_action) { + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { + resolve(); + }); + } else { + errorToUser(e, "fastboot boot"); + } }); - }; + }); break; case "systemimage": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "systemimage")); - }); - }; + return new Promise(function(resolve, reject) { + systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { + resolve(); + }).catch(e => errorToUser(e, "systemimage")); + }); break; case "fastboot:update": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Updating system"); - global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); - fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { - utils.log.debug(step.type + " done"); - resolve(); - }).catch(e => errorToUser(e, "fastboot update")); - }); - }; + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Updating system"); + global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); + fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { + resolve(); + }).catch(e => errorToUser(e, "fastboot update")); + }); break; case "user_action": - return () => { - return new Promise(function(resolve, reject) { - utils.log.debug("step: " + JSON.stringify(step)); - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { - utils.log.debug(step.type + " done"); - resolve(); - }); + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { + resolve(); }); - }; + }); break; default: throw "error: unrecognized step type: " + step.type } } -function install(steps) { +function assembleInstallSteps(steps) { var installPromises = []; steps.forEach((step) => { - installPromises.push(installStep(step)); + installPromises.push(() => { + return new Promise(function(resolve, reject) { + if (step.condition && global.installProperties.settings[step.condition.var] != step.condition.value) { + // If the condition is not met, no need to do anything + utils.log.debug("skipping step: " + JSON.stringify(step)); + resolve(); + } else { + utils.log.debug("running step: " + JSON.stringify(step)); + installStep(step).then(() => { + resolve(); + utils.log.debug(step.type + " done"); + }).catch((error) => { + if (step.fallback_user_action) { + installStep({ + type: "user_action", action: step.fallback_user_action + }).then(resolve).catch(reject); + } else { + utils.log.error(step.type + " failed with error " + error); + reject(error); + } + }); + } + }); + }); }); + installPromises.push(() => { global.mainEvent.emit("user:write:done"); global.mainEvent.emit("user:write:status", global.installConfig.operating_systems[global.installProperties.osIndex].name + " successfully installed!", false); global.mainEvent.emit("user:write:under", global.installConfig.operating_systems[global.installProperties.osIndex].success_message || "All done! Enjoy exploring your new OS!"); }); - // Actually run the steps - installPromises.reduce( - (promiseChain, currentFunction) => promiseChain.then(currentFunction), - Promise.resolve() - ); + return installPromises; } module.exports = { @@ -225,5 +207,12 @@ module.exports = { } return osSelects; }, - install: install + install: (steps) => { + var installPromises = assembleInstallSteps(steps); + // Actually run the steps + installPromises.reduce( + (promiseChain, currentFunction) => promiseChain.then(currentFunction), + Promise.resolve() + ); + } } From f84ba6665f847904b5bf7d1c37caf9931c822f08 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 12:00:34 +0200 Subject: [PATCH 36/53] Remove unneeded specific error-handling --- src/devices.js | 46 +++++++++------------------------------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/src/devices.js b/src/devices.js index 05df2f93..35d1b699 100644 --- a/src/devices.js +++ b/src/devices.js @@ -72,14 +72,7 @@ function installStep(step) { global.mainEvent.emit("user:write:working", "particles");+ global.mainEvent.emit("user:write:status", "Rebooting"); global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); - adb.reboot(step.to_state).then(() => { - resolve(); - }).catch((error) => { - utils.log.error("reboot failed: " + error); - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.to_state], () => { - resolve(); - }); - }); + adb.reboot(step.to_state).then(resolve).catch(reject); }); break; case "fastboot:flash": @@ -88,9 +81,7 @@ function installStep(step) { global.mainEvent.emit("user:write:status", "Flashing firmware"); global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) - fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(() => { - resolve(); - }).catch(e => errorToUser(e, "fastboot flash")); + fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(resolve).catch(reject); }); break; case "fastboot:erase": @@ -98,9 +89,7 @@ function installStep(step) { global.mainEvent.emit("user:write:working", "particles");+ global.mainEvent.emit("user:write:status", "Ceaning up"); global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); - fastboot.erase(step.partition).then(() => { - resolve(); - }).catch(e => errorToUser(e, "fastboot erase")); + fastboot.erase(step.partition).then(resolve).catch(reject); }); break; case "fastboot:boot": @@ -108,41 +97,25 @@ function installStep(step) { global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Rebooting"); global.mainEvent.emit("user:write:under", "Your device is being rebooted..."); - fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(() => { - resolve(); - }).catch((e) => { - if (step.fallback_user_action) { - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.fallback_user_action], () => { - resolve(); - }); - } else { - errorToUser(e, "fastboot boot"); - } - }); + fastboot.boot(path.join(downloadPath, global.installProperties.device, step.group, step.file), step.partition).then(resolve).catch(reject); }); break; case "systemimage": return new Promise(function(resolve, reject) { - systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(() => { - resolve(); - }).catch(e => errorToUser(e, "systemimage")); + systemImage.installLatestVersion(Object.assign({device: global.installConfig.codename}, global.installProperties.settings)).then(resolve).catch(reject); }); break; case "fastboot:update": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Updating system"); - global.mainEvent.emit("user:write:under", "Applying fastboot update zip"); - fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(() => { - resolve(); - }).catch(e => errorToUser(e, "fastboot update")); + global.mainEvent.emit("user:write:under", "Applying fastboot update zip. This may take a while..."); + fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(resolve).catch(reject); }); break; case "user_action": return new Promise(function(resolve, reject) { - global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], () => { - resolve(); - }); + global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], resolve); }); break; default: @@ -170,8 +143,7 @@ function assembleInstallSteps(steps) { type: "user_action", action: step.fallback_user_action }).then(resolve).catch(reject); } else { - utils.log.error(step.type + " failed with error " + error); - reject(error); + errorToUser(error, step.type); } }); } From 9f565318b798f72b7c88c482150f99b17f7f8b2c Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 12:04:26 +0200 Subject: [PATCH 37/53] Implement optional steps --- src/devices.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/devices.js b/src/devices.js index 35d1b699..92f23b28 100644 --- a/src/devices.js +++ b/src/devices.js @@ -142,6 +142,8 @@ function assembleInstallSteps(steps) { installStep({ type: "user_action", action: step.fallback_user_action }).then(resolve).catch(reject); + } else if (step.optional) { + resolve(); } else { errorToUser(error, step.type); } From ad44a37073bc33bc9813116fd974142741c8b8ed Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 12:14:52 +0200 Subject: [PATCH 38/53] Move system-image error handling to central place --- src/system-image.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/system-image.js b/src/system-image.js index 50e9fd4f..fbf9f2dd 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -49,11 +49,11 @@ var installLatestVersion = (options) => { global.mainEvent.emit("user:write:progress", progress*100); }).then(() => { resolve(); - }).catch(e => utils.errorToUser(e, "Push failed: Failed push")); - }).catch(e => utils.errorToUser(e, "Push failed: Failed to create target dir")); - }).catch(e => utils.errorToUser(e, "Push failed: Failed to mount")); - }).catch(e => utils.errorToUser(e, "Push failed: Failed wipe cache")); - }).catch(e => utils.errorToUser(e, "Download failed")); + }).catch(e => reject("Push failed: Failed push: " + e)); + }).catch(e => reject("Push failed: Failed to create target dir: " + e)); + }).catch(e => reject("Push failed: Failed to mount: " + e)); + }).catch(e => reject("Push failed: Failed wipe cache: " + e)); + }).catch(e => reject("Download failed: " + e)); }); } From e9da31cfb07865ffd3bf6fb4b4ead01c5c5bafc2 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 12:35:56 +0200 Subject: [PATCH 39/53] Implement fastboot reboot-bootloader step --- src/devices.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/devices.js b/src/devices.js index 92f23b28..2eb550bf 100644 --- a/src/devices.js +++ b/src/devices.js @@ -69,7 +69,7 @@ function installStep(step) { break; case "adb:reboot": return new Promise(function(resolve, reject) { - global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Rebooting"); global.mainEvent.emit("user:write:under", "Rebooting to " + step.to_state); adb.reboot(step.to_state).then(resolve).catch(reject); @@ -86,7 +86,7 @@ function installStep(step) { break; case "fastboot:erase": return new Promise(function(resolve, reject) { - global.mainEvent.emit("user:write:working", "particles");+ + global.mainEvent.emit("user:write:working", "particles"); global.mainEvent.emit("user:write:status", "Ceaning up"); global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); fastboot.erase(step.partition).then(resolve).catch(reject); @@ -113,6 +113,14 @@ function installStep(step) { fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(resolve).catch(reject); }); break; + case "fastboot:reboot_bootloader": + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Rebooting"); + global.mainEvent.emit("user:write:under", "Rebooting to bootloader"); + fastboot.rebootBootloader().then(resolve).catch(reject); + }); + break; case "user_action": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:action", global.installConfig.user_actions[step.action], resolve); From 75d11e20e5337164c03239fbb8ac143a30aa5329 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 12:54:04 +0200 Subject: [PATCH 40/53] Restart when connection lost --- src/devices.js | 7 ++++++- src/main.js | 2 +- src/system-image.js | 24 +++++++++++++----------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/devices.js b/src/devices.js index 2eb550bf..023aaf7b 100644 --- a/src/devices.js +++ b/src/devices.js @@ -153,7 +153,12 @@ function assembleInstallSteps(steps) { } else if (step.optional) { resolve(); } else { - errorToUser(error, step.type); + if (error.includes("no device")) { + // TODO restart the current step or the install promises rather than the installer + mainEvent.emit("user:connection-lost"); + } else { + utils.errorToUser(error, step.type); + } } }); } diff --git a/src/main.js b/src/main.js index 9d23f4b2..9ac22c70 100755 --- a/src/main.js +++ b/src/main.js @@ -173,7 +173,7 @@ mainEvent.on("user:error", (err) => { // Connection to the device was lost mainEvent.on("user:connection-lost", (callback) => { - if (mainWindow) mainWindow.webContents.send("user:connection-lost", callback); + if (mainWindow) mainWindow.webContents.send("user:connection-lost", callback || mainWindow.reload()); }); // The device battery is too low to install diff --git a/src/system-image.js b/src/system-image.js index fbf9f2dd..c4b24e51 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -42,17 +42,19 @@ var installLatestVersion = (options) => { mainEvent.emit("user:write:working", "push"); mainEvent.emit("user:write:status", "Sending"); mainEvent.emit("user:write:under", "Sending files to the device"); - adb.wipeCache().then(() => { - adb.shell("mount -a").then(() => { - adb.shell("mkdir -p /cache/recovery").then(() => { - adb.pushArray(files, (progress) => { - global.mainEvent.emit("user:write:progress", progress*100); - }).then(() => { - resolve(); - }).catch(e => reject("Push failed: Failed push: " + e)); - }).catch(e => reject("Push failed: Failed to create target dir: " + e)); - }).catch(e => reject("Push failed: Failed to mount: " + e)); - }).catch(e => reject("Push failed: Failed wipe cache: " + e)); + adb.waitForDevice().then(() => { + adb.wipeCache().then(() => { + adb.shell("mount -a").then(() => { + adb.shell("mkdir -p /cache/recovery").then(() => { + adb.pushArray(files, (progress) => { + global.mainEvent.emit("user:write:progress", progress*100); + }).then(() => { + resolve(); + }).catch(e => reject("Push failed: Failed push: " + e)); + }).catch(e => reject("Push failed: Failed to create target dir: " + e)); + }).catch(e => reject("Push failed: Failed to mount: " + e)); + }).catch(e => reject("Push failed: Failed wipe cache: " + e)); + }).catch(e => reject("no device")); }).catch(e => reject("Download failed: " + e)); }); } From 5e8fa429d094fdcc5889f4cc0105bec5f172c4aa Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 13:15:29 +0200 Subject: [PATCH 41/53] Properly erase userdata on fastboot update if requested to do so --- src/devices.js | 13 ++++++++----- src/main.js | 4 ++-- src/utils.js | 4 ++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/devices.js b/src/devices.js index 023aaf7b..dc46e5b8 100644 --- a/src/devices.js +++ b/src/devices.js @@ -49,7 +49,7 @@ function installStep(step) { case "download": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "download"); - global.mainEvent.emit("user:write:status", "Downloading " + step.group); + global.mainEvent.emit("user:write:status", "Downloading " + step.group, true); global.mainEvent.emit("user:write:under", "Downloading"); utils.downloadFiles(addPathToImages(step.files, global.installProperties.device, step.group), (progress, speed) => { global.mainEvent.emit("user:write:progress", progress*100); @@ -78,7 +78,7 @@ function installStep(step) { case "fastboot:flash": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Flashing firmware"); + global.mainEvent.emit("user:write:status", "Flashing firmware", true); global.mainEvent.emit("user:write:under", "Flashing firmware partitions using fastboot"); utils.log.debug(JSON.stringify(addPathToFiles(step.flash, global.installProperties.device))) fastboot.flashArray(addPathToFiles(step.flash, global.installProperties.device)).then(resolve).catch(reject); @@ -87,7 +87,7 @@ function installStep(step) { case "fastboot:erase": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Ceaning up"); + global.mainEvent.emit("user:write:status", "Ceaning up", true); global.mainEvent.emit("user:write:under", "Erasing " + step.partition + " partition"); fastboot.erase(step.partition).then(resolve).catch(reject); }); @@ -108,9 +108,12 @@ function installStep(step) { case "fastboot:update": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "particles"); - global.mainEvent.emit("user:write:status", "Updating system"); + global.mainEvent.emit("user:write:status", "Updating system", true); global.mainEvent.emit("user:write:under", "Applying fastboot update zip. This may take a while..."); - fastboot.update(path.join(downloadPath, global.installProperties.device, step.group, step.file)).then(resolve).catch(reject); + fastboot.update( + path.join(downloadPath, global.installProperties.device, step.group, step.file), + global.installProperties.settings.wipe + ).then(resolve).catch(reject); }); break; case "fastboot:reboot_bootloader": diff --git a/src/main.js b/src/main.js index 9ac22c70..21cecb2b 100755 --- a/src/main.js +++ b/src/main.js @@ -25,8 +25,8 @@ const app = electron.app; const BrowserWindow = electron.BrowserWindow; global.packageInfo = require('../package.json'); -const Adb = require('promise-android-tools').Adb; -const Fastboot = require('promise-android-tools').Fastboot; +const Adb = require('../../promise-android-tools/src/module.js').Adb; +const Fastboot = require('../../promise-android-tools/src/module.js').Fastboot; const Api = require("../../ubports-api-node-module/src/module.js").Installer; const exec = require('child_process').exec; diff --git a/src/utils.js b/src/utils.js index 5cfd3f4c..954b6a8b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -197,6 +197,10 @@ function getFallbackPlatformTools() { { package: path.join(toolInPackage, maybeEXE(thisPlatform, "adb")), cache: path.join(toolInCache, maybeEXE(thisPlatform, "adb")) + }, + { + package: path.join(toolInPackage, maybeEXE(thisPlatform, "mke2fs")), + cache: path.join(toolInCache, maybeEXE(thisPlatform, "mke2fs")) } ] } From 97a7d6878efa8cb188f0c3343332f84362cc1af3 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 17:44:03 +0200 Subject: [PATCH 42/53] Support remote configs *hype* --- src/devices.js | 4 ++-- src/main.js | 30 ++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/devices.js b/src/devices.js index dc46e5b8..f60ce3a4 100644 --- a/src/devices.js +++ b/src/devices.js @@ -184,13 +184,13 @@ module.exports = { adb.getDeviceName().then((device) => { adb.getOs().then((operatingSystem) => { global.api.resolveAlias(device).then((resolvedDevice) => { - global.mainEvent.emit("device:detected", resolvedDevice, (operatingSystem=="ubuntutouch"), true); + global.mainEvent.emit("device:detected", resolvedDevice); }).catch((error) => { utils.errorToUser(error, "Resolve device alias"); }); }).catch((error) => { utils.errorToUser(error, "Wait for device"); }); }).catch((error) => { utils.errorToUser(error, "get device name"); }); }).catch(e => utils.log.debug("no device detected: " + e)); }, - getOsSelects: (osArray) => { // TODO move to api module + getOsSelects: (osArray) => { // Can't be moved to support custom config files var osSelects = []; for (var i = 0; i < osArray.length; i++) { osSelects.push(""); diff --git a/src/main.js b/src/main.js index 21cecb2b..d311d6d8 100755 --- a/src/main.js +++ b/src/main.js @@ -132,15 +132,29 @@ ipcMain.on("createBugReport", (event, title) => { ipcMain.on("device:selected", (event, device) => { adb.stopWaiting(); global.installProperties.device = device; - if(devices.getOsSelects(global.installConfig.operating_systems).length > 1) { - // ask for os selection if there's one os - mainWindow.webContents.send( - "user:os", - global.installConfig, devices.getOsSelects(global.installConfig.operating_systems) - ); + function continueWithConfig() { + if(global.installConfig.operating_systems.length > 1) { + // ask for os selection if there's one os + mainWindow.webContents.send( + "user:os", + global.installConfig, devices.getOsSelects(global.installConfig.operating_systems) + ); + } else { + // immediately jump to configure if there's only one os + mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); + } + } + if(global.installConfig) { + // local config specified + continueWithConfig(); } else { - // immediately jump to configure if there's only one os - mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); + // local config specified + api.getDevice(device).then((config) => { + global.installConfig = config; + continueWithConfig(); + }).catch(() => { + mainEvent.emit("user:device-unsupported", device); + }); } }); From d559a42bbcd48e4a8f63d49d9a38a172f165623e Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 19:01:01 +0200 Subject: [PATCH 43/53] Update deps --- package-lock.json | 964 ++++++++++++++++++++++------------------------ package.json | 7 +- 2 files changed, 466 insertions(+), 505 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7eaa26cb..48a51027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,24 +149,6 @@ "type-detect": "4.0.8" } }, - "@sinonjs/formatio": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", - "requires": { - "samsam": "1.3.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", - "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, "@sinonjs/text-encoding": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", @@ -209,7 +191,8 @@ "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true }, "acorn": { "version": "3.3.0", @@ -258,12 +241,6 @@ "repeat-string": "^1.5.2" } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "optional": true - }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -307,11 +284,15 @@ } } }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -710,9 +691,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, "buffer": { "version": "5.2.1", @@ -1069,8 +1050,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "color-convert": { "version": "1.9.3", @@ -1224,12 +1204,12 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "coveralls": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.4.tgz", - "integrity": "sha512-eyqUWA/7RT0JagiL0tThVhjbIjoiEUyWCjtUJoOPcWoeofP5WK/jb2OJYoBFrR6DvplR+AxOyuBqk4JHkk5ykA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.7.tgz", + "integrity": "sha512-mUuH2MFOYB2oBaA4D4Ykqi9LaEYpMMlsiOMJOrv358yAjP6enPIk55fod2fNJ8AvwoYXStWQls37rA+s5e7boA==", "requires": { "growl": "~> 1.10.0", - "js-yaml": "^3.11.0", + "js-yaml": "^3.13.1", "lcov-parse": "^0.0.10", "log-driver": "^1.2.7", "minimist": "^1.2.0", @@ -1320,6 +1300,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -1483,11 +1464,6 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, "default-require-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", @@ -1509,15 +1485,30 @@ "integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, "dmg-builder": { "version": "6.7.2", @@ -2159,6 +2150,40 @@ "is-arrayish": "^0.2.1" } }, + "es-abstract": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -2175,44 +2200,11 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" - }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -2303,11 +2295,6 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, "fd-slicer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", @@ -2373,6 +2360,21 @@ "pinkie-promise": "^2.0.0" } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + } + } + }, "flora-colossus": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-1.0.1.tgz", @@ -2760,13 +2762,14 @@ } }, "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "requires": { + "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "2 || 3", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -2831,9 +2834,9 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" }, "handlebars": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz", - "integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", + "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -2841,6 +2844,12 @@ "uglify-js": "^3.1.4" }, "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -2851,12 +2860,12 @@ } }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.3.tgz", + "integrity": "sha512-KfQUgOqTkLp2aZxrMbCuKCDGW9slFYu2A23A36Gs7sGzTLcRBDORdOi5E21KWHFIfkY8kzgi/Pr1cXCh0yIp5g==", "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" } } @@ -2885,15 +2894,20 @@ } }, "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + }, "has-to-string-tag-x": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", @@ -2917,9 +2931,9 @@ } }, "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "home-path": { "version": "1.0.6", @@ -3033,6 +3047,11 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + }, "is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", @@ -3042,6 +3061,11 @@ "ci-info": "^2.0.0" } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, "is-electron-renderer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", @@ -3076,7 +3100,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3150,6 +3173,14 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "requires": { + "has-symbols": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -3188,49 +3219,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } - }, "istanbul-lib-coverage": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", @@ -3275,11 +3263,6 @@ "supports-color": "^6.1.0" }, "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -3536,15 +3519,6 @@ "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, "listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -3593,17 +3567,21 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" }, - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==" + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } }, "longest": { "version": "1.0.1", @@ -3792,56 +3770,109 @@ } }, "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" }, "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" } }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==" + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } }, - "has-flag": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { - "has-flag": "^2.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" } } } @@ -3849,7 +3880,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "neo-async": { "version": "2.6.1", @@ -3867,32 +3899,13 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==" - } + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" } }, "nodeify": { @@ -3926,6 +3939,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, "requires": { "abbrev": "1" } @@ -4013,8 +4027,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nyc": { "version": "14.1.1", @@ -4071,19 +4084,6 @@ "locate-path": "^3.0.0" } }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -4150,12 +4150,44 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==" + }, "object-keys": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", "dev": true }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4172,26 +4204,6 @@ "wordwrap": "~0.0.2" } }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } - }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -4496,11 +4508,6 @@ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz", "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==" }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -4540,92 +4547,17 @@ } }, "promise-android-tools": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.3.tgz", - "integrity": "sha512-8usNEZkE4pHw2efMfsiPe7MbgcBDdVQjfbA3w1BC2otNiTq8WtgY/YYlRAjzceFJyMsD5H13yYr97nyigOa9ig==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.4.tgz", + "integrity": "sha512-HRMx2RMst47JAegrpdjZ9h9f/BSBlxwf8T4w5ililBjtzuiUnQURXbvhfpXqfbCoht22rmycZ1LjFqVxMMBbOQ==", "requires": { "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "coveralls": "^3.0.0", - "mocha": "^4.0.1", + "mocha": "^6.2.2", "nyc": "^14.1.1", "sinon": "^7.5.0", "sinon-chai": "^3.3.0" - }, - "dependencies": { - "@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", - "requires": { - "@sinonjs/commons": "^1.3.0", - "array-from": "^2.1.1", - "lodash": "^4.17.15" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "lolex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", - "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==" - }, - "nise": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", - "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", - "requires": { - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - } - }, - "sinon": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", - "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.3", - "diff": "^3.5.0", - "lolex": "^4.2.0", - "nise": "^1.5.2", - "supports-color": "^5.5.0" - } - }, - "sinon-chai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", - "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "proto-list": { @@ -5056,11 +4988,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==" - }, "sanitize-filename": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", @@ -5149,31 +5076,69 @@ } }, "sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" }, "dependencies": { + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==" + }, + "nise": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", + "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5181,9 +5146,9 @@ } }, "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", + "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==" }, "sort-keys": { "version": "1.1.2", @@ -5304,13 +5269,30 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -5321,7 +5303,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5361,8 +5342,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "strip-outer": { "version": "1.0.1", @@ -5394,125 +5374,28 @@ } }, "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "requires": { - "has-flag": "^1.0.0" + "has-flag": "^3.0.0" } }, "system-image-node-module": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/system-image-node-module/-/system-image-node-module-1.0.9.tgz", - "integrity": "sha512-4KAtlVaSwW0YvsO81eeDUjcO5fPH7GWDir0FQSda8+u+dftpH8YtQIRbJQ7228CI1k5pcDHLPn2JTWRFYWPkxw==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/system-image-node-module/-/system-image-node-module-1.0.10.tgz", + "integrity": "sha512-nF1C+qERRheegcgWDUCsqIN6W+jqnDllLRjJdX2ynBe0NyLF2eSLh5mKcVTUr4iYDjLXetayfa1vEHvdLpDcXg==", "requires": { - "chai": "^4.1.2", + "chai": "^4.2.0", "checksum": "^0.1.1", "coveralls": "^3.0.0", "download": "^7.1.0", - "istanbul": "^0.4.5", "mkdirp": "^0.5.1", - "mocha": "^4.0.1", + "mocha": "^6.2.2", + "nyc": "^14.1.1", "request": "^2.83.0", - "sinon": "^4.1.3", - "sinon-chai": "^2.14.0" - }, - "dependencies": { - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==" - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" - } - }, - "sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", - "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "requires": { - "has-flag": "^2.0.0" - } - } + "sinon": "^7.5.0", + "sinon-chai": "^3.3.0" } }, "tar-stream": { @@ -5602,19 +5485,6 @@ "locate-path": "^3.0.0" } }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5788,14 +5658,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5813,6 +5675,21 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "ubports-api-node-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ubports-api-node-module/-/ubports-api-node-module-2.0.1.tgz", + "integrity": "sha512-pDewpPpm0L2hOFqAZDzhxsMPAimKsZdiIJ0d3DQwENIACgdzQ3q2lGMoqonf/4FB6gnlsZ17OW3cMOj5XXT1EQ==", + "requires": { + "chai": "^4.2.0", + "coveralls": "^3.0.7", + "mkdirp": "^0.5.1", + "mocha": "^6.2.2", + "nyc": "^14.1.1", + "request": "^2.83.0", + "sinon": "^7.5.0", + "sinon-chai": "^3.3.0" + } + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -6009,6 +5886,14 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "widest-line": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", @@ -6209,6 +6094,81 @@ } } }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + } + } + }, "yauzl": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", diff --git a/package.json b/package.json index 30a0f8d9..da5ead57 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "electron": "^1.8.8", "electron-builder": "^20.36.2", "electron-packager": "^12.2.0", - "sinon": "^4.1.3", + "sinon": "^7.5.0", "unzipper": "^0.9.4" }, "dependencies": { @@ -52,9 +52,10 @@ "jquery": "^3.1.1", "jquery-i18next": "^1.2.0", "mkdirp": "^0.5.1", - "promise-android-tools": "^1.0.3", + "promise-android-tools": "^1.0.4", "request": "^2.79.0", - "system-image-node-module": "^1.0.9", + "system-image-node-module": "^1.0.10", + "ubports-api-node-module": "^2.0.1", "winston": "^2.3.1" } } From b2b26d99fb00a00cc04e6c4d1d2b4f584b788b4e Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sat, 19 Oct 2019 19:01:21 +0200 Subject: [PATCH 44/53] Remove local deps --- src/main.js | 63 ++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/main.js b/src/main.js index d311d6d8..416e5f33 100755 --- a/src/main.js +++ b/src/main.js @@ -25,9 +25,9 @@ const app = electron.app; const BrowserWindow = electron.BrowserWindow; global.packageInfo = require('../package.json'); -const Adb = require('../../promise-android-tools/src/module.js').Adb; -const Fastboot = require('../../promise-android-tools/src/module.js').Fastboot; -const Api = require("../../ubports-api-node-module/src/module.js").Installer; +const Adb = require('promise-android-tools').Adb; +const Fastboot = require('promise-android-tools').Fastboot; +const Api = require("ubports-api-node-module").Installer; const exec = require('child_process').exec; const path = require('path'); @@ -131,31 +131,7 @@ ipcMain.on("createBugReport", (event, title) => { // The user selected a device ipcMain.on("device:selected", (event, device) => { adb.stopWaiting(); - global.installProperties.device = device; - function continueWithConfig() { - if(global.installConfig.operating_systems.length > 1) { - // ask for os selection if there's one os - mainWindow.webContents.send( - "user:os", - global.installConfig, devices.getOsSelects(global.installConfig.operating_systems) - ); - } else { - // immediately jump to configure if there's only one os - mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); - } - } - if(global.installConfig) { - // local config specified - continueWithConfig(); - } else { - // local config specified - api.getDevice(device).then((config) => { - global.installConfig = config; - continueWithConfig(); - }).catch(() => { - mainEvent.emit("user:device-unsupported", device); - }); - } + mainEvent.emit("device", device); }); // The user selected an os @@ -275,11 +251,38 @@ mainEvent.on("user:configure", (osInstructs) => { } }); +mainEvent.on("device", (device) => { + global.installProperties.device = device; + function continueWithConfig() { + if(global.installConfig.operating_systems.length > 1) { + // ask for os selection if there's one os + mainWindow.webContents.send( + "user:os", + global.installConfig, devices.getOsSelects(global.installConfig.operating_systems) + ); + } else { + // immediately jump to configure if there's only one os + mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); + } + } + if(global.installConfig && global.installConfig.operating_systems) { + // local config specified + continueWithConfig(); + } else { + // local config specified + api.getDevice(device).then((config) => { + global.installConfig = config; + continueWithConfig(); + }).catch(() => { + mainEvent.emit("user:device-unsupported", device); + }); + } +}); + // The user selected a device mainEvent.on("device:detected", (device) => { utils.log.info("device detected: " + device) - global.installProperties.device = device; - mainWindow.webContents.send("user:os", global.installConfig, devices.getOsSelects(global.installConfig.operating_systems)); + mainEvent.emit("device", device); }); //============================================================================== From 34847a6a1692e2cbd2067c0e2f1ea5aff032ae03 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Sun, 20 Oct 2019 11:12:16 +0200 Subject: [PATCH 45/53] Fix config files with only one OS --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index 416e5f33..73e805f8 100755 --- a/src/main.js +++ b/src/main.js @@ -262,6 +262,7 @@ mainEvent.on("device", (device) => { ); } else { // immediately jump to configure if there's only one os + global.installProperties.osIndex = 0; mainEvent.emit("user:configure", global.installConfig.operating_systems[0]); } } From 8b4f0eee608d7453afd31275bef25d5c980754d0 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Mon, 21 Oct 2019 18:04:19 +0200 Subject: [PATCH 46/53] Add support for remote config value providers --- src/devices.js | 22 ++++++++++++++++++++++ src/html/modals/options.pug | 2 +- src/main.js | 4 +++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/devices.js b/src/devices.js index f60ce3a4..0e453605 100644 --- a/src/devices.js +++ b/src/devices.js @@ -204,5 +204,27 @@ module.exports = { (promiseChain, currentFunction) => promiseChain.then(currentFunction), Promise.resolve() ); + }, + setRemoteValues: (osInstructs) => { + return Promise.all(osInstructs.options.map(option => { + return new Promise(function(resolve, reject) { + if (!option.remote_values) { + resolve(option); // no remote values, nothing to do + } else { + switch (option.remote_values.type) { + case "systemimagechannels": + systemImage.getDeviceChannels(global.installConfig.codename).then((channels) => { + option.values = channels.map(channel => { + return { value: channel, label: channel.replace("ubports-touch/", "") } + }).reverse(); + resolve(option); + }).catch(e => reject("error fetching system image channels: " + e)); + break; + default: + reject("unknown remote_values provider: " + option.remote_values.type); + } + } + }); + })); } } diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 5e02d225..37726ab2 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -65,6 +65,6 @@ modals.show("options"); if (!optionsAdded) { optionsAdded = true; - osInstructs.options.forEach(addOption); + osInstructs.forEach(addOption); } }); diff --git a/src/main.js b/src/main.js index 73e805f8..31c0572b 100755 --- a/src/main.js +++ b/src/main.js @@ -243,7 +243,9 @@ mainEvent.on("user:configure", (osInstructs) => { if(osInstructs.options) { // If there's something to configure, configure it! if (mainWindow) { - mainWindow.webContents.send("user:configure", osInstructs); + devices.setRemoteValues(osInstructs).then((osInstructs) => { + mainWindow.webContents.send("user:configure", osInstructs); + }).catch(e => utils.errorToUser(e, "configure")); } } else { // If there's nothing to configure, don't configure anything From 1656566e76e4f46bfb5c809f6f7970964aa87462 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Tue, 22 Oct 2019 00:09:59 +0200 Subject: [PATCH 47/53] Implement oem unlock and reconnect --- src/devices.js | 54 +++++++++++++++++++++++++-------------------- src/main.js | 3 +++ src/system-image.js | 4 ++-- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/devices.js b/src/devices.js index 0e453605..fd5c20f1 100644 --- a/src/devices.js +++ b/src/devices.js @@ -145,25 +145,29 @@ function assembleInstallSteps(steps) { resolve(); } else { utils.log.debug("running step: " + JSON.stringify(step)); - installStep(step).then(() => { - resolve(); - utils.log.debug(step.type + " done"); - }).catch((error) => { - if (step.fallback_user_action) { - installStep({ - type: "user_action", action: step.fallback_user_action - }).then(resolve).catch(reject); - } else if (step.optional) { + function runStep() { + installStep(step).then(() => { resolve(); - } else { - if (error.includes("no device")) { - // TODO restart the current step or the install promises rather than the installer - mainEvent.emit("user:connection-lost"); + utils.log.debug(step.type + " done"); + }).catch((error) => { + if (step.fallback_user_action) { + installStep({ + type: "user_action", action: step.fallback_user_action + }).then(resolve).catch(reject); + } else if (error.includes("lock")) { + global.mainEvent.emit("user:oem-lock", () => { install(steps); }); + } else if (step.optional) { + resolve(); } else { - utils.errorToUser(error, step.type); + if (error.includes("no device") || error.includes("No such device")) { + mainEvent.emit("user:connection-lost", runStep); + } else { + utils.errorToUser(error, step.type); + } } - } - }); + }); + } + runStep(); } }); }); @@ -178,6 +182,15 @@ function assembleInstallSteps(steps) { return installPromises; } +function install(steps) { + var installPromises = assembleInstallSteps(steps); + // Actually run the steps + installPromises.reduce( + (promiseChain, currentFunction) => promiseChain.then(currentFunction), + Promise.resolve() + ); +} + module.exports = { waitForDevice: () => { adb.waitForDevice().then(() => { @@ -197,14 +210,7 @@ module.exports = { } return osSelects; }, - install: (steps) => { - var installPromises = assembleInstallSteps(steps); - // Actually run the steps - installPromises.reduce( - (promiseChain, currentFunction) => promiseChain.then(currentFunction), - Promise.resolve() - ); - }, + install: install, setRemoteValues: (osInstructs) => { return Promise.all(osInstructs.options.map(option => { return new Promise(function(resolve, reject) { diff --git a/src/main.js b/src/main.js index 31c0572b..71f0c345 100755 --- a/src/main.js +++ b/src/main.js @@ -182,6 +182,9 @@ mainEvent.on("restart", () => { mainEvent.on("user:oem-lock", (callback) => { mainWindow.webContents.send("user:oem-lock"); ipcMain.once("user:oem-lock:ok", () => { + mainEvent.emit("user:write:working", "particles"); + mainEvent.emit("user:write:status", "Unlocking", true); + mainEvent.emit("user:write:under", "You might see a confirmation dialog on your device."); fastboot.oemUnlock().then(() => { callback(true); }).catch((err) => { diff --git a/src/system-image.js b/src/system-image.js index c4b24e51..c40c8775 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -29,7 +29,7 @@ const getDeviceChannels = (device) => { var installLatestVersion = (options) => { return new Promise(function(resolve, reject) { mainEvent.emit("user:write:working", "download"); - mainEvent.emit("user:write:status", "Downloading Ubuntu Touch"); + mainEvent.emit("user:write:status", "Downloading Ubuntu Touch", true); mainEvent.emit("user:write:under", "Downloading"); systemImage.downloadLatestVersion(options, (progress, speed) => { mainEvent.emit("user:write:progress", progress*100); @@ -40,7 +40,7 @@ var installLatestVersion = (options) => { mainEvent.emit("download:done"); mainEvent.emit("user:write:progress", 0); mainEvent.emit("user:write:working", "push"); - mainEvent.emit("user:write:status", "Sending"); + mainEvent.emit("user:write:status", "Sending", true); mainEvent.emit("user:write:under", "Sending files to the device"); adb.waitForDevice().then(() => { adb.wipeCache().then(() => { From a922b2bf9ccd04c220f512dfbb79911d48b6a48b Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 13:38:32 +0200 Subject: [PATCH 48/53] Show tooltip for options --- src/html/modals/options.pug | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/html/modals/options.pug b/src/html/modals/options.pug index 37726ab2..89f0aa1f 100644 --- a/src/html/modals/options.pug +++ b/src/html/modals/options.pug @@ -49,6 +49,25 @@ _subdiv.appendChild(_input); _div.appendChild(_subdiv); $("#options-form").append(_div); + + // tooltip describing the option + if (option.tooltip) { + let _tooltipdiv = document.createElement("div"); + _tooltipdiv.className = "col-xs-3"; + _div.appendChild(_tooltipdiv); + let _tooltip = document.createElement("p"); + _tooltip.className = "col-xs-9"; + _tooltip.appendChild(document.createTextNode(option.tooltip + " ")); + if (option.link) { + let _tooltiplink = document.createElement("a"); + _tooltiplink.id = option.var + "_link" + _tooltiplink.appendChild(document.createTextNode("More...")); + _tooltip.appendChild(_tooltiplink); + } + _div.appendChild(_tooltip); + // HACK: link target can not be set before, because the element needs to have already been created + if (option.link) $("#" + option.var + "_link").click(() => shell.openExternal(option.link)); + } // send data for this option to main for processing $("#btn-options-close").click(() => { From 6f555fb3b58e3eb9a83407b84f8e886c0a1addc8 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 13:39:23 +0200 Subject: [PATCH 49/53] Some logging in systemimage --- src/system-image.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/system-image.js b/src/system-image.js index c40c8775..37f8667b 100644 --- a/src/system-image.js +++ b/src/system-image.js @@ -44,8 +44,11 @@ var installLatestVersion = (options) => { mainEvent.emit("user:write:under", "Sending files to the device"); adb.waitForDevice().then(() => { adb.wipeCache().then(() => { + utils.log.debug("adb wiped cache"); adb.shell("mount -a").then(() => { + utils.log.debug("adb mounted recovery"); adb.shell("mkdir -p /cache/recovery").then(() => { + utils.log.debug("adb created /cache/recovery directory"); adb.pushArray(files, (progress) => { global.mainEvent.emit("user:write:progress", progress*100); }).then(() => { From 235b50de545c1fc505837a408a2a55524e5474f5 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 16:00:01 +0200 Subject: [PATCH 50/53] Add adb format step --- src/devices.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/devices.js b/src/devices.js index fd5c20f1..8b4ad93d 100644 --- a/src/devices.js +++ b/src/devices.js @@ -67,6 +67,16 @@ function installStep(step) { }).catch(reject); }); break; + case "adb:format": + return new Promise(function(resolve, reject) { + global.mainEvent.emit("user:write:working", "particles"); + global.mainEvent.emit("user:write:status", "Preparing system for installation", true); + global.mainEvent.emit("user:write:under", "Formatting " + step.partition); + adb.waitForDevice().then(() => { + adb.format(step.partition).then(resolve).catch(reject); + }).catch(reject); + }); + break; case "adb:reboot": return new Promise(function(resolve, reject) { global.mainEvent.emit("user:write:working", "particles"); From 53ce6075f2e6be5651c7740f3027f67bd82d36dd Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 17:52:03 +0200 Subject: [PATCH 51/53] Bump android tools --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 48a51027..2accf04f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4547,9 +4547,9 @@ } }, "promise-android-tools": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.4.tgz", - "integrity": "sha512-HRMx2RMst47JAegrpdjZ9h9f/BSBlxwf8T4w5ililBjtzuiUnQURXbvhfpXqfbCoht22rmycZ1LjFqVxMMBbOQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/promise-android-tools/-/promise-android-tools-1.0.5.tgz", + "integrity": "sha512-AV07cDEVrf8zEd7PyG5qHsyXZBUXn4eX1+wgtFeKySw4xGYzhgqiXA/NHBXcR0jpYV1sOkLhxmP2hSw8pxNnDw==", "requires": { "chai": "^4.1.2", "chai-as-promised": "^7.1.1", diff --git a/package.json b/package.json index da5ead57..ef764885 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "jquery": "^3.1.1", "jquery-i18next": "^1.2.0", "mkdirp": "^0.5.1", - "promise-android-tools": "^1.0.4", + "promise-android-tools": "^1.0.5", "request": "^2.79.0", "system-image-node-module": "^1.0.10", "ubports-api-node-module": "^2.0.1", From d6b84d12d92cbbaae75c1be7d992b2f8504bab7e Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 19:46:27 +0200 Subject: [PATCH 52/53] WINSTOOOOOOON! Fixes #935 --- package-lock.json | 223 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- src/main.js | 23 ++++- src/utils.js | 36 +++----- 4 files changed, 231 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2accf04f..b8da8c0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1052,6 +1052,15 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1065,10 +1074,33 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } }, "combined-stream": { "version": "1.0.8", @@ -1283,11 +1315,6 @@ "array-find-index": "^1.0.1" } }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1505,6 +1532,16 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -2128,6 +2165,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -2142,6 +2187,11 @@ "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", "dev": true }, + "env-variable": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", + "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2280,11 +2330,6 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -2295,6 +2340,11 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "fd-slicer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", @@ -2304,6 +2354,11 @@ "pend": "~1.2.0" } }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, "file-type": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", @@ -3485,6 +3540,14 @@ "graceful-fs": "^4.1.9" } }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "requires": { + "colornames": "^1.1.1" + } + }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -3583,6 +3646,25 @@ "chalk": "^2.0.1" } }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -4196,6 +4278,11 @@ "wrappy": "1" } }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, "optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", @@ -5066,6 +5153,21 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "single-line-log": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", @@ -5544,6 +5646,11 @@ } } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5636,6 +5743,11 @@ "escape-string-regexp": "^1.0.2" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -5942,22 +6054,81 @@ "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" }, "winston": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", - "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", - "requires": { - "async": "~1.0.0", - "colors": "1.0.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "stack-trace": "0.0.x" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "requires": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "requires": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" }, "dependencies": { - "async": { + "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, diff --git a/package.json b/package.json index ef764885..5bd0b715 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,6 @@ "request": "^2.79.0", "system-image-node-module": "^1.0.10", "ubports-api-node-module": "^2.0.1", - "winston": "^2.3.1" + "winston": "^3.1.2" } } diff --git a/src/main.js b/src/main.js index 71f0c345..56e0af16 100755 --- a/src/main.js +++ b/src/main.js @@ -29,6 +29,7 @@ const Adb = require('promise-android-tools').Adb; const Fastboot = require('promise-android-tools').Fastboot; const Api = require("ubports-api-node-module").Installer; +var winston = require('winston'); const exec = require('child_process').exec; const path = require('path'); const url = require('url'); @@ -89,7 +90,6 @@ global.installProperties = { device: global.installConfig ? global.installConfig.codename : cli.device, cli: cli.cli, settings: cli.settings ? JSON.parse(cli.settings) : {}, - verbose: (cli.verbose || cli.debug), debug: cli.debug }; @@ -97,6 +97,26 @@ global.packageInfo.isSnap = utils.isSnap(); utils.exportExecutablesFromPackage(); +//============================================================================== +// WINSTOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOON! +//============================================================================== + +global.logger = winston.createLogger({ + format: winston.format.json(), + defaultMeta: { service: 'user-service' }, + transports: [ + new winston.transports.File({ + filename: path.join(utils.getUbuntuTouchDir(), 'ubports-installer.log'), + options: { flags: 'w' }, + level: "debug" + }), + new winston.transports.Console({ + format: winston.format.simple(), + level: cli.verbose ? "debug" : "info" + }) + ] +}); + //============================================================================== // RENDERER SIGNAL HANDLING //============================================================================== @@ -296,7 +316,6 @@ mainEvent.on("device:detected", (device) => { //============================================================================== function createWindow () { - utils.setLogLevel(global.installProperties.verbose ? "debug" : "info"); utils.log.info("Welcome to the UBports Installer version " + global.packageInfo.version + "!"); utils.log.info("This is " + (global.packageInfo.updateAvailable ? "not " : "") + "the latest stable version!"); mainWindow = new BrowserWindow({ diff --git a/src/utils.js b/src/utils.js index 954b6a8b..915d17e0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -23,24 +23,24 @@ const path = require("path"); const checksum = require('checksum'); const mkdirp = require('mkdirp'); const cp = require('child_process'); -const winston = require('winston'); const getos = require('getos'); global.packageInfo = require('../package.json'); +if (!fs.existsSync(getUbuntuTouchDir())) { + mkdirp.sync(getUbuntuTouchDir()); +} + const platforms = { "linux": "linux", "darwin": "mac", "win32": "win" } -if (global.installProperties) - winston.level = global.installProperties.verbose ? 'debug' : 'info'; - var log = { - error: (l) => {winston.log("error", l)}, - warn: (l) => {winston.log("warn", l)}, - info: (l) => {winston.log("info", l)}, - debug: (l) => {winston.log("debug", l)} + error: (l) => { global.logger.log("error", l); }, + warn: (l) => { global.logger.log("warn", l); }, + info: (l) => { global.logger.log("info", l); }, + debug: (l) => { global.logger.log("debug", l); } } function createBugReport(title, installProperties, callback) { @@ -50,17 +50,16 @@ function createBugReport(title, installProperties, callback) { order: 'desc' }; - winston.query(options, function (err, results) { + global.logger.query(options, function (err, results) { if (err) { throw err; } var errorLog = ""; results.file.forEach((err) => { - errorLog+=err.level+" " - errorLog+=err.timestamp+" " + errorLog+=err.level+": " errorLog+=err.message+"\n" - }) + }); http.post({ url: "http://paste.ubuntu.com", @@ -149,16 +148,6 @@ function getUbuntuTouchDir() { return path.join(osCacheDir, "ubports"); } -if (!fs.existsSync(getUbuntuTouchDir())) { - mkdirp.sync(getUbuntuTouchDir()); -} - -winston.add(winston.transports.File, { - filename: path.join(getUbuntuTouchDir(), 'ubports-installer.log'), - level: 'debug', // Print debug logs to the file - options: { flags: 'w' } // Clear log before writing to it -}); - function die(e) { log.error(e); process.exit(-1); @@ -313,6 +302,5 @@ module.exports = { getUbuntuTouchDir: getUbuntuTouchDir, createBugReport: createBugReport, getUpdateAvailable: getUpdateAvailable, - die: die, - setLogLevel: (level) => { winston.level = level; } + die: die } From be110804268996729ba47564b2602aa19d1277b8 Mon Sep 17 00:00:00 2001 From: Jan Sprinz Date: Wed, 23 Oct 2019 20:27:47 +0200 Subject: [PATCH 53/53] Properly export executables, fix #934 --- build.js | 2 +- src/utils.js | 48 +++++++++++------------------------------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/build.js b/build.js index baf25a7b..c9842e22 100755 --- a/build.js +++ b/build.js @@ -186,7 +186,7 @@ if (cli.platformTools) { if (!cli.downloadOnly) build(); }); }).catch(() => { - console.error("Failed to download files!") + console.error("Failed to download files!"); process.exit(1); }); } else { diff --git a/src/utils.js b/src/utils.js index 915d17e0..e4f1ad3a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -155,43 +155,17 @@ function die(e) { // WORKAROUND: the chile spawned by child_process.exec can not access files inside the asar package function exportExecutablesFromPackage() { - getFallbackPlatformTools().forEach((tool) => { - fs.copy(tool.package, tool.cache, () => { - fs.chmodSync(tool.cache, 0o755); - }); - }); -} - -function maybeEXE(platform, tool) { - if(platform === "win32") tool+=".exe"; - return tool; -} - -function getPlatform() { - var thisPlatform = os.platform(); - if(!platforms[thisPlatform]) die("Unsuported platform"); - return platforms[thisPlatform]; -} - -function getFallbackPlatformTools() { - var thisPlatform = os.platform(); - if(!platforms[thisPlatform]) die("Unsupported platform"); - var toolInPackage = path.join(__dirname, "/../platform-tools/", platforms[thisPlatform]); - var toolInCache = path.join(utils.getUbuntuTouchDir(), 'platform-tools'); - return [ - { - package: path.join(toolInPackage, maybeEXE(thisPlatform, "fastboot")), - cache: path.join(toolInCache, maybeEXE(thisPlatform, "fastboot")) - }, - { - package: path.join(toolInPackage, maybeEXE(thisPlatform, "adb")), - cache: path.join(toolInCache, maybeEXE(thisPlatform, "adb")) - }, - { - package: path.join(toolInPackage, maybeEXE(thisPlatform, "mke2fs")), - cache: path.join(toolInCache, maybeEXE(thisPlatform, "mke2fs")) + fs.copy( + path.join(__dirname, "..", "platform-tools", platforms[os.platform()]), + path.join(utils.getUbuntuTouchDir(), 'platform-tools'), + () => { + if (os.platform() !== "win32") { + ["adb", "fastboot", "mke2fs"].map(exe => path.join(utils.getUbuntuTouchDir(), 'platform-tools', exe)).forEach((tool) => { + fs.chmodSync(tool, 0o755); + }); + } } - ] + ); } function isSnap() { @@ -274,7 +248,7 @@ function downloadFiles(urls, progress, next) { reject(err); return; }); - }); + }).catch(reject); }); }); })).then(() => {