This is an automated email from the ASF dual-hosted git repository.
erisu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-paramedic.git
The following commit(s) were added to refs/heads/master by this push:
new e376dc0 feat!: drop shelljs, execa, exec & some refactor (#286)
e376dc0 is described below
commit e376dc016fa8d70d6c461f202da970ef186c650a
Author: エリス <[email protected]>
AuthorDate: Tue Dec 16 11:24:35 2025 +0900
feat!: drop shelljs, execa, exec & some refactor (#286)
* feat!: drop shelljs, execa, exec, & partial refactor
* refactor!: stabilize testing connection & on events
---
.github/workflows/ios.yml | 2 +-
lib/ParamedicApp.js | 134 ++++++++++-----
lib/ParamedicAppUninstall.js | 78 ++++-----
lib/ParamedicKill.js | 80 +++++----
lib/ParamedicLogCollector.js | 103 ++++++-----
lib/ParamedicTargetChooser.js | 44 +++--
lib/ParamediciOSPermissions.js | 59 ++++---
lib/PluginsManager.js | 64 ++++---
lib/paramedic.js | 378 +++++++++++++++++++----------------------
lib/utils/execWrapper.js | 53 ------
lib/utils/index.js | 5 +-
lib/utils/spawn.js | 136 +++++++++++++++
lib/utils/utilities.js | 85 +++------
package-lock.json | 134 +--------------
package.json | 2 -
15 files changed, 673 insertions(+), 684 deletions(-)
diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml
index a1846fa..df865aa 100644
--- a/.github/workflows/ios.yml
+++ b/.github/workflows/ios.yml
@@ -64,7 +64,7 @@ jobs:
- os-version: macos-26
ios-version: 26.x
- xcode-version: 26.x
+ xcode-version: 26.1
steps:
- uses: actions/checkout@v6
diff --git a/lib/ParamedicApp.js b/lib/ParamedicApp.js
index a5cb143..cb166e5 100644
--- a/lib/ParamedicApp.js
+++ b/lib/ParamedicApp.js
@@ -20,10 +20,10 @@
*/
const tmp = require('tmp');
-const shell = require('shelljs');
const path = require('path');
+const fs = require('node:fs');
const PluginsManager = require('./PluginsManager');
-const { logger, exec, execPromise, utilities } = require('./utils');
+const { logger, spawnAsync, utilities } = require('./utils');
class ParamedicApp {
constructor (config, storedCWD, runner) {
@@ -34,21 +34,28 @@ class ParamedicApp {
this.platformId = this.config.getPlatformId();
this.isAndroid = this.platformId === utilities.ANDROID;
- this.isBrowser = this.platformId === utilities.BROWSER;
this.isIos = this.platformId === utilities.IOS;
logger.info('---------------------------------------------------------');
logger.info('1. Create Cordova app with platform and plugin(s) to
test');
- logger.info('- platform: ' + this.config.getPlatformId());
+ logger.info('- platform: ' + this.platformId);
logger.info('- plugin(s): ' + this.config.getPlugins().join(', '));
logger.info('---------------------------------------------------------');
}
- createTempProject () {
+ /**
+ * Creates a Cordova project inside a newly created temporary directory.
+ *
+ * @returns {Promise<Object>} Object contains the directory path on the
name property.
+ */
+ async createTempProject () {
this.tempFolder = tmp.dirSync();
tmp.setGracefulCleanup();
- logger.info('cordova-paramedic: creating temp project at ' +
this.tempFolder.name);
- exec(this.config.getCli() + ' create ' + this.tempFolder.name +
utilities.PARAMEDIC_COMMON_CLI_ARGS);
+ logger.info('[paramedic] Creating temp project at ' +
this.tempFolder.name);
+ await spawnAsync(
+ this.config.getCli(),
+ ['create', this.tempFolder.name,
...utilities.PARAMEDIC_COMMON_ARGS]
+ );
return this.tempFolder;
}
@@ -61,72 +68,107 @@ class ParamedicApp {
.then(() => this.checkDumpAndroidConfigXml());
}
- installPlugins () {
- logger.info('cordova-paramedic: installing plugins');
+ /**
+ * Installs testing related framework plugins and user defined plugin.
+ *
+ * (For All Platforms)
+ * - cordova-plugin-test-framework
+ * - paramedic-plugin
+ * (For iOS Platform)
+ * - ios-geolocation-permissions-plugin (iOS)
+ * (For CI)
+ * - ci-plugin
+ * (User Defined Plugins)
+ * (User Defined Plugin's Test)
+ */
+ async installPlugins () {
const pluginsManager = new PluginsManager(this.tempFolder.name,
this.storedCWD, this.config);
-
const ciFrameworkPlugins =
['github:apache/cordova-plugin-test-framework', path.join(__dirname, '..',
'paramedic-plugin')];
if (this.isIos) {
ciFrameworkPlugins.push(path.join(__dirname, '..',
'ios-geolocation-permissions-plugin'));
}
-
if (this.config.isCI()) {
ciFrameworkPlugins.push(path.join(__dirname, '..', 'ci-plugin'));
}
// Install testing framework
- logger.info('cordova-paramedic: installing ci framework plugins: ' +
ciFrameworkPlugins.join(', '));
- pluginsManager.installPlugins(ciFrameworkPlugins);
- logger.info('cordova-paramedic: installing plugins:' +
this.config.getPlugins().join(', '));
- pluginsManager.installPlugins(this.config.getPlugins());
- logger.info('cordova-paramedic: installing tests for existing
plugins');
- pluginsManager.installTestsForExistingPlugins();
+ logger.info(`[paramedic] Installing CI Plugins:\n\t -
${ciFrameworkPlugins.join('\n\t - ')}`);
+ await pluginsManager.installPlugins(ciFrameworkPlugins);
+ logger.info(`[paramedic] Installing Plugins:\n\t -
${this.config.getPlugins().join('\n\t - ')}`);
+ await pluginsManager.installPlugins(this.config.getPlugins());
+ logger.info('[paramedic] Installing tests for existing plugins.');
+ await pluginsManager.installTestsForExistingPlugins();
}
+ /**
+ * Edits the the testing application's content source to
"cdvtests/index.html"
+ */
setUpStartPage () {
- logger.normal('cordova-paramedic: setting the app start page to the
test page');
- shell.sed('-i', 'src="index.html"', 'src="cdvtests/index.html"',
'config.xml');
+ logger.normal('[paramedic] Setting the app start page to the test
page');
+ const filePath = path.join(this.tempFolder.name, 'config.xml');
+ let config = fs.readFileSync(filePath, utilities.DEFAULT_ENCODING);
+ config = config.replace('src="index.html"',
'src="cdvtests/index.html"');
+ fs.writeFileSync(filePath, config, utilities.DEFAULT_ENCODING);
}
+ /**
+ * Installs the Cordova platform for testing
+ *
+ * @returns {Promise<Object|Error>}
+ */
installPlatform () {
- const platform = this.config.getPlatform();
- logger.info('cordova-paramedic: adding platform ' + platform + '
(with: ' + utilities.PARAMEDIC_COMMON_CLI_ARGS +
utilities.PARAMEDIC_PLATFORM_ADD_ARGS + ')');
-
- return execPromise(this.config.getCli() + ' platform add ' + platform
+ utilities.PARAMEDIC_COMMON_CLI_ARGS + utilities.PARAMEDIC_PLATFORM_ADD_ARGS)
- .then(() => {
- logger.info('cordova-paramedic: successfully finished adding
platform ' + platform);
- });
+ return spawnAsync(
+ this.config.getCli(),
+ ['platform', 'add', this.platformId,
...utilities.PARAMEDIC_COMMON_ARGS],
+ { cwd: this.tempFolder.name }
+ );
}
- checkPlatformRequirements () {
- if (this.isBrowser) return Promise.resolve();
-
- logger.normal('cordova-paramedic: checking the requirements for
platform: ' + this.platformId);
- return execPromise(this.config.getCli() + ' requirements ' +
this.platformId + utilities.PARAMEDIC_COMMON_CLI_ARGS)
- .then(() => {
- logger.info('cordova-paramedic: successfully finished checking
the requirements for platform: ' + this.platformId);
- });
+ /**
+ * Gets the platform reqirements
+ *
+ * @returns {Promise<Object|Error>}
+ */
+ async checkPlatformRequirements () {
+ const requirements = await spawnAsync(
+ this.config.getCli(),
+ ['requirements', this.platformId,
...utilities.PARAMEDIC_COMMON_ARGS],
+ { cwd: this.tempFolder.name }
+ );
+ logger.normal(requirements.stdout);
}
+ /**
+ * Fetches and dumps out the AndroidManifest.xml content.
+ *
+ * @return If not running for Android platform, return out.
+ */
checkDumpAndroidManifest () {
- if (!this.isAndroid) return Promise.resolve();
+ if (!this.isAndroid) {
+ return;
+ }
- logger.normal('cordova-paramedic: start AndroidManifest.xml Dump');
- return execPromise('cat
./platforms/android/app/src/main/AndroidManifest.xml')
- .then(() => {
- logger.normal('cordova-paramedic: end AndroidManifest.xml
Dump');
- });
+ logger.normal('[paramedic] AndroidManifest.xml Dump');
+ const androidManifest = path.join(this.tempFolder.name,
'platforms/android/app/src/main/AndroidManifest.xml');
+ const xml = fs.readFileSync(androidManifest,
utilities.DEFAULT_ENCODING);
+ logger.normal(xml);
}
+ /**
+ * Fetches and dumps out the Android's compiled config.xml content.
+ *
+ * @return If not running for Android platform, return out.
+ */
checkDumpAndroidConfigXml () {
- if (!this.isAndroid) return Promise.resolve();
+ if (!this.isAndroid) {
+ return;
+ }
- logger.normal('cordova-paramedic: start config.xml Dump');
- return execPromise('cat
./platforms/android/app/src/main/res/xml/config.xml')
- .then(() => {
- logger.info('cordova-paramedic: end config.xml Dump');
- });
+ logger.normal('[paramedic] config.xml Dump');
+ const config = path.join(this.tempFolder.name,
'platforms/android/app/src/main/res/xml/config.xml');
+ const xml = fs.readFileSync(config, utilities.DEFAULT_ENCODING);
+ logger.normal(xml);
}
}
diff --git a/lib/ParamedicAppUninstall.js b/lib/ParamedicAppUninstall.js
index f386b95..c14cb24 100644
--- a/lib/ParamedicAppUninstall.js
+++ b/lib/ParamedicAppUninstall.js
@@ -17,9 +17,7 @@
under the License.
*/
-const { exec } = require('node:child_process');
-
-const { logger, utilities } = require('./utils');
+const { utilities, spawnAsync } = require('./utils');
class ParamedicAppUninstall {
constructor (appPath, platform) {
@@ -27,18 +25,25 @@ class ParamedicAppUninstall {
this.platform = platform;
}
- async uninstallApp (targetObj, app) {
- if (!targetObj || !targetObj.target) {
+ /**
+ * Uninstall application from emulator based on provided target and
application identifier.
+ *
+ * @param {Object} target Object of emulator/target related information.
+ * @param {String} appId The application/bundle identifier.
+ * @returns {Promise<Boolean>}
+ */
+ async uninstallApp (target, appId) {
+ if (!target || !target.target) {
return false;
}
switch (this.platform) {
case utilities.ANDROID:
- await this.uninstallAppAndroid(targetObj, app);
+ await this.uninstallAppAndroid(target, appId);
return true;
case utilities.IOS:
- await this.uninstallAppIOS(targetObj, app);
+ await this.uninstallAppIOS(target, appId);
return true;
default:
@@ -46,51 +51,32 @@ class ParamedicAppUninstall {
}
}
- uninstallAppAndroid (targetObj, app) {
- const uninstallCommand = 'adb -s ' + targetObj.target + ' uninstall '
+ app;
- return this.executeUninstallCommand(uninstallCommand);
+ /**
+ * Uninstalls the application from the Android target by application ID
+ *
+ * @param {Object} target The device/emulator data which contains the
device target.
+ * @param {String} appId The application ID
+ */
+ uninstallAppAndroid (target, appId) {
+ return spawnAsync(
+ 'adb',
+ ['-s', target.target, 'uninstall', appId],
+ { cwd: this.appPath, timeout: 60000 }
+ );
}
/**
- * Uninstalls the Application form target by Bundle Identifier
+ * Uninstalls the application from the iOS target by Bundle ID
*
* @param {Object} target The device/emulator data which contains the
device and UUID.
- * @param {String} appBundleIdentifier The Application Bundle Identifier
+ * @param {String} bundleId The application's bundle ID
*/
- uninstallAppIOS (target, appBundleIdentifier) {
- return this.executeUninstallCommand(`xcrun simctl uninstall
${target.simId} ${appBundleIdentifier}`);
- }
-
- // TODO: Remove this for a centralized spawnAsync utility
- async executeUninstallCommand (uninstallCommand) {
- logger.info('[paramedic] Running command: ' + uninstallCommand);
-
- const execPromise = new Promise((resolve, reject) => {
- exec(uninstallCommand, (error, stdout, stderr) => {
- if (!error) {
- resolve();
- } else {
- logger.error('[paramedic] Failed to uninstall the app');
- logger.error('[paramedic] Error code: ' + error.code);
- logger.error('[paramedic] stderr: ' + stderr);
- reject(error);
- }
- });
- });
-
- const timeoutPromise = new Promise((resolve, reject) => {
- setTimeout(() => reject(new Error('timeout')), 60000);
- });
-
- try {
- await Promise.race([execPromise, timeoutPromise]);
- } catch (err) {
- if (err.message === 'timeout') {
- logger.warn('[paramedic] App uninstall timed out!');
- } else {
- logger.warn('[paramedic] App uninstall error: ' + err.message);
- }
- }
+ uninstallAppIOS (target, bundleId) {
+ return spawnAsync(
+ 'xcrun',
+ ['simctl', 'uninstall', target.simId, bundleId],
+ { cwd: this.appPath, timeout: 60000 }
+ );
}
}
diff --git a/lib/ParamedicKill.js b/lib/ParamedicKill.js
index 67bc0a9..73e565b 100644
--- a/lib/ParamedicKill.js
+++ b/lib/ParamedicKill.js
@@ -19,8 +19,7 @@
under the License.
*/
-const shelljs = require('shelljs');
-const { logger, exec, utilities } = require('./utils');
+const { logger, utilities, spawnAsync } = require('./utils');
class ParamedicKill {
constructor (platform) {
@@ -28,10 +27,6 @@ class ParamedicKill {
}
kill () {
- // shell config
- shelljs.config.fatal = false;
- shelljs.config.silent = false;
-
// get platform tasks
const platformTasks = this.tasksOnPlatform(this.platform);
@@ -70,45 +65,60 @@ class ParamedicKill {
return tasks;
}
- killTasks (taskNames) {
- if (!taskNames || taskNames.length < 1) return;
-
- const command = this.getKillCommand(taskNames);
-
- logger.info('Running the following command:');
- logger.info(' ' + command);
-
- const killTasksResult = exec(command);
- if (killTasksResult.code !== 0) {
- console.warn('WARNING: kill command returned ' +
killTasksResult.code);
+ /**
+ * Attempts to kill the provided tasks by name.
+ *
+ * The kill command is determined by the isWindows flag.
+ * If running on a Windows environment, 'taskkill' will be
+ * used. If on a macOS or Linux, 'killall' is used.
+ *
+ * If process fails, only a warning will be displayed.
+ *
+ * @param {Array} taskNames List of tasks to kill.
+ * @returns {Promise}
+ */
+ async killTasks (taskNames) {
+ if (!taskNames || taskNames.length < 1) {
+ return;
}
- }
- getKillCommand (taskNames) {
- const cli = utilities.isWindows()
- ? 'taskkill /t /F'
- : 'killall -9';
+ const cmd = utilities.isWindows()
+ ? 'taskkill'
+ : 'killall';
- const args = utilities.isWindows()
- ? taskNames.map(name => `/IM "${name}"`)
- : taskNames.map(name => `"${name}"`);
+ let args = utilities.isWindows()
+ ? ['/t', '/F']
+ : ['-9'];
- return cli + ' ' + args.join(' ');
- }
+ const taskArgs = utilities.isWindows()
+ ? taskNames.map(name => ['/IM', `"${name}"`])
+ : taskNames.map(name => [`"${name}"`]);
- killAdbServer () {
- logger.info('Killing the adb server');
- const killServerCommand = 'adb kill-server';
+ // Attach to the args the processes that will be killed
+ for (const task of taskArgs) {
+ args = [...args, ...task];
+ }
- logger.info('Running the following command:');
- logger.info(' ' + killServerCommand);
+ // Attempt to kill the processes
+ const killTasksResult = await spawnAsync(cmd, args);
+ if (killTasksResult.code !== 0) {
+ console.warn('[paramedic] WARNING: Kill command returned ' +
killTasksResult.code);
+ }
+ }
- const killServerResult = exec(killServerCommand);
+ /**
+ * Attempts to kill the ADB Server.
+ *
+ * If process fails, only a warning will be displayed.
+ */
+ async killAdbServer () {
+ logger.info('[paramedic] Killing the adb server');
+ const killServerResult = await spawnAsync('adb', ['kill-server']);
if (killServerResult.code !== 0) {
- logger.error('Failed to kill the adb server with the code: ' +
killServerResult.code);
+ logger.error('[paramedic] Failed to kill the adb server with the
code: ' + killServerResult.code);
}
- logger.info('Killed the adb server.');
+ logger.info('[paramedic] Killed the adb server.');
}
}
diff --git a/lib/ParamedicLogCollector.js b/lib/ParamedicLogCollector.js
index 41c6d45..19b603e 100644
--- a/lib/ParamedicLogCollector.js
+++ b/lib/ParamedicLogCollector.js
@@ -19,10 +19,11 @@
under the License.
*/
-const shelljs = require('shelljs');
-const fs = require('fs');
-const path = require('path');
-const { logger, exec, utilities } = require('./utils');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+
+const { logger, spawnAsync, utilities } = require('./utils');
class ParamedicLogCollector {
constructor (platform, appPath, outputDir, targetObj) {
@@ -32,77 +33,75 @@ class ParamedicLogCollector {
this.targetObj = targetObj;
}
- logIOS () {
- if (!this.targetObj) {
- logger.warn('It looks like there is no target to get logs from.');
+ /**
+ * If the simulator id and the simulator's system.log exists, it will be
copied
+ * over to the provided output path.
+ */
+ #logIOS () {
+ if (!this.targetObj.simId) {
+ logger.info('[paramedic] Missing Simulator ID from target to
locate logs.');
return;
}
- const simId = this.targetObj.simId;
+ const homedir = os.homedir();
+ const systemLogs = path.join(homedir, 'Library', 'Logs',
'CoreSimulator', this.targetObj.simId, 'system.log');
- if (simId) {
- const homedir = require('os').homedir();
- // Now we can print out the log file
- const logPath = path.join(homedir, 'Library', 'Logs',
'CoreSimulator', simId, 'system.log');
- const logCommand = 'cat ' + logPath;
- this.generateLogs(logCommand);
+ if (fs.existsSync(systemLogs)) {
+ const outputFilePath = this.#getLogFileName();
+ fs.cpSync(systemLogs, outputFilePath);
} else {
- logger.error('Failed to find the ID of the simulator');
- }
- }
-
- logAndroid () {
- if (!this.targetObj) {
- logger.warn('It looks like there is no target to get logs from.');
- return;
- }
-
- const logCommand = 'adb -s ' + this.targetObj.target + ' logcat -d -v
time';
- const numDevices = utilities.countAndroidDevices();
-
- if (numDevices !== 1) {
- logger.error('There must be exactly one emulator/device attached');
- return;
+ logger.info('[paramedic] No logs found for the requested Simulator
ID.');
}
-
- this.generateLogs(logCommand);
}
- generateLogs (logCommand) {
- logger.info('Running Command: ' + logCommand);
+ /**
+ * Captures the logs from adb logcat and stores it to the provided output
path
+ *
+ * @returns {Promise}
+ */
+ async #logAndroid () {
+ const content = await spawnAsync(
+ 'adb',
+ ['-s', this.targetObj.target, 'logcat', '-d', '-v', 'time']
+ );
- const logFile = this.getLogFileName();
- const result = exec(logCommand);
-
- if (result.code > 0) {
- logger.error('Failed to run command: ' + logCommand);
- logger.error('Failure code: ' + result.code);
- return;
- }
+ const logFileOutput = this.#getLogFileName();
try {
- fs.writeFileSync(logFile, result.stdout);
- logger.info('Logfiles are written to: ' + logFile);
- } catch (ex) {
- logger.error('Cannot write the log results to the file. ' + ex);
+ fs.writeFileSync(logFileOutput, content.stdout);
+ logger.info(`[paramedic] Log files written to: ${logFileOutput}`);
+ } catch (err) {
+ logger.error(`[paramedic] Faild to write logs with error:
${err.message}`);
}
}
- getLogFileName () {
+ /**
+ * Returns file path where the log content will be written/copied to.
+ *
+ * @returns {String}
+ */
+ #getLogFileName () {
return path.join(this.outputDir, this.platform + '_logs.txt');
}
- collectLogs () {
- shelljs.config.fatal = false;
- shelljs.config.silent = false;
+ /**
+ * Collects the logs logs and writes out to output location.
+ *
+ * @returns {Promise}
+ */
+ async collectLogs () {
+ if (!this.targetObj) {
+ logger.warn('[paramedic] There is no target to fetch logs from.');
+ return;
+ }
switch (this.platform) {
case utilities.ANDROID:
- this.logAndroid();
+ await this.#logAndroid();
break;
case utilities.IOS:
- this.logIOS(this.appPath);
+ this.#logIOS();
break;
default:
diff --git a/lib/ParamedicTargetChooser.js b/lib/ParamedicTargetChooser.js
index cb031a0..e82a0df 100644
--- a/lib/ParamedicTargetChooser.js
+++ b/lib/ParamedicTargetChooser.js
@@ -30,23 +30,35 @@ class ParamedicTargetChooser {
this.cli = config.getCli();
}
- async chooseTarget (emulator, target) {
+ /**
+ * Collects target information by platform id.
+ *
+ * @param {String} target E.g. "iPhone-17-Pro, 26.1"
+ * @returns {Promise<Object>} Target data
+ */
+ async chooseTarget (target) {
switch (this.platform) {
case utilities.ANDROID:
- return this.chooseTargetForAndroid(emulator, target);
+ return this.chooseTargetForAndroid(target);
case utilities.IOS:
- return this.chooseTargetForIOS(emulator, target);
+ return this.chooseTargetForIOS(target);
default:
}
}
- async chooseTargetForAndroid (emulator, target) {
- logger.info('cordova-paramedic: Choosing Target for Android');
+ /**
+ * Tries to start if emualtor not set and returns the Android emulator ID.
+ *
+ * @param {String} target The desired emulator to use.
+ * @returns {Promise<Object>}
+ */
+ async chooseTargetForAndroid (target) {
+ logger.info('[paramedic] Choosing Target for Android');
if (target) {
- logger.info('cordova-paramedic: Target defined as: ' + target);
+ logger.info('[paramedic] Target defined as: ' + target);
return { target };
}
@@ -54,7 +66,7 @@ class ParamedicTargetChooser {
}
async startAnAndroidEmulator (target) {
- logger.info('cordova-paramedic: Starting an Android emulator');
+ logger.info('[paramedic] Starting an Android emulator');
const emuPathInNodeModules = path.join(this.appPath, 'node_modules',
'cordova-android', 'lib', 'emulator.js');
const emuPathInPlatform = path.join(this.appPath, 'platforms',
'android', 'cordova', 'lib', 'emulator.js');
@@ -75,7 +87,7 @@ class ParamedicTargetChooser {
return tryStart(numberTriesRemaining - 1);
}
- logger.error('cordova-paramedic: Could not start an Android
emulator');
+ logger.error('[paramedic] Could not start an Android emulator');
return null;
};
@@ -89,11 +101,17 @@ class ParamedicTargetChooser {
return await tryStart(ANDROID_RETRY_TIMES);
}
- async chooseTargetForIOS (emulator, target) {
- logger.info('cordova-paramedic: Choosing Target for iOS');
-
- const simulatorModelId = utilities.getSimulatorModelId(this.cli,
target);
- const simulatorData = utilities.getSimulatorData(simulatorModelId);
+ /**
+ * Returns iOS related target data.
+ *
+ * @param {String} target The desired emulator device type and iOS version
+ * @returns {Promise<Object>}
+ */
+ async chooseTargetForIOS (target) {
+ logger.info('[paramedic] Choosing Target for iOS');
+
+ const simulatorModelId = await
utilities.getSimulatorModelId(this.appPath, this.cli, target);
+ const simulatorData = await
utilities.getSimulatorData(simulatorModelId);
return {
target: simulatorModelId,
diff --git a/lib/ParamediciOSPermissions.js b/lib/ParamediciOSPermissions.js
index 4e8c9de..18b7aff 100644
--- a/lib/ParamediciOSPermissions.js
+++ b/lib/ParamediciOSPermissions.js
@@ -21,9 +21,7 @@
const path = require('path');
const fs = require('fs');
-const shelljs = require('shelljs');
-const util = require('util');
-const { logger, utilities } = require('./utils');
+const { logger, utilities, spawnAsync } = require('./utils');
const TCC_FOLDER_PERMISSION = 0o755;
@@ -34,7 +32,12 @@ class ParamediciOSPermissions {
this.targetObj = targetObj;
}
- updatePermissions (serviceList) {
+ /**
+ * Add or update service list to grant permissions for testing.
+ *
+ * @param {Array} serviceList List of services that should grant permission
+ */
+ async updatePermissions (serviceList) {
const simId = this.targetObj.simId;
logger.info('Sim Id is: ' + simId);
@@ -49,34 +52,34 @@ class ParamediciOSPermissions {
}
logger.info('Copying TCC Db file to ' + tccDirectory);
- shelljs.cp(this.tccDb, tccDirectory);
+ fs.cpSync(this.tccDb, tccDirectory);
}
- for (let i = 0; i < serviceList.length; i++) {
- let command =
utilities.getSqlite3InsertionCommand(destinationTCCFile, serviceList[i],
this.appName);
- logger.info('Running Command: ' + command);
+ for (const service of serviceList) {
+ const app = this.appName;
// If the service has an entry already, the insert command will
fail.
// in this case we'll process with updating existing entry
- console.log('$ ' + command);
- const proc = shelljs.exec(command, { silent: true, async: false });
-
- if (proc.code) {
- logger.warn('Failed to insert permissions for ' + this.appName
+ ' into ' + destinationTCCFile +
- ' Will try to update existing permissions.');
-
- // (service, client, client_type, allowed, prompt_count, csreq)
- command = util.format('sqlite3 %s "update access ' +
- 'set client_type=0, allowed=1, prompt_count=1, csreq=NULL
' +
- 'where service=\'%s\' and client=\'%s\'"',
destinationTCCFile, serviceList[i], this.appName);
-
- logger.info('Running Command: ' + command);
- // Now we really don't care about the result as there is
nothing we can do with this
- console.log('$ ' + command);
- const patchProc = shelljs.exec(command, { silent: true, async:
false });
-
- if (patchProc.code) {
- logger.warn('Failed to update existing permissions for ' +
this.appName + ' into ' + destinationTCCFile +
- ' Continuing anyway.');
+ const insetProc = await spawnAsync(
+ 'sqlite3',
+ [
+ destinationTCCFile,
+ `"INSERT INTO access (service, client, client_type,
allowed, prompt_count, csreq) VALUES('${service}', '${app}', 0, 1, 1, NULL)"`
+ ]
+ );
+
+ if (insetProc.code) {
+ logger.warn(`[paramedic] Failed to insert permissions for
${app} into ${destinationTCCFile}. Will try to update existing permissions.`);
+
+ const updateProc = await spawnAsync(
+ 'sqlite3',
+ [
+ destinationTCCFile,
+ `"UPDATE access SET client_type=0, allowed=1,
prompt_count=1, csreq=NULL WHERE service='${service}' AND client='${app}'"`
+ ]
+ );
+
+ if (updateProc.code) {
+ logger.warn(`[paramedic] Failed to update existing
permissions for ${app} into ${destinationTCCFile}. Continuing anyway.`);
}
}
}
diff --git a/lib/PluginsManager.js b/lib/PluginsManager.js
index 490123f..f0a090f 100644
--- a/lib/PluginsManager.js
+++ b/lib/PluginsManager.js
@@ -19,7 +19,7 @@
const path = require('path');
const fs = require('fs');
-const { logger, exec, utilities } = require('./utils');
+const { logger, spawnAsync, utilities } = require('./utils');
const { PluginInfoProvider } = require('cordova-common');
class PluginsManager {
@@ -29,27 +29,41 @@ class PluginsManager {
this.config = config;
}
- installPlugins (plugins) {
- for (let n = 0; n < plugins.length; n++) {
- this.installSinglePlugin(plugins[n]);
+ /**
+ * Installs list of plugins to the temporary Cordova testing project.
+ *
+ * @param {Array} plugins
+ */
+ async installPlugins (plugins) {
+ for (const plugin of plugins) {
+ await this.installSinglePlugin(plugin);
}
}
- installTestsForExistingPlugins () {
+ /**
+ * Loops though the installed plugins and installs tests to the temporary
Cordova
+ * testing project, if the plugins have.
+ */
+ async installTestsForExistingPlugins () {
const installedPlugins = new
PluginInfoProvider().getAllWithinSearchPath(path.join(this.appRoot, 'plugins'));
- installedPlugins.forEach((plugin) => {
- // there is test plugin available
+ for (const plugin of installedPlugins) {
+ // Install test if it exists
if (fs.existsSync(path.join(plugin.dir, 'tests', 'plugin.xml'))) {
- this.installSinglePlugin(path.join(plugin.dir, 'tests'));
+ await this.installSinglePlugin(path.join(plugin.dir, 'tests'));
}
- });
+ }
// this will list installed plugins and their versions
- this.showPluginsVersions();
+ await this.showPluginsVersions();
}
- installSinglePlugin (plugin) {
+ /**
+ * Installs a single plugin to the temporary Cordova testing project.
+ *
+ * @param {String} plugin
+ */
+ async installSinglePlugin (plugin) {
let pluginPath = plugin;
let args = '';
@@ -64,19 +78,29 @@ class PluginsManager {
plugin = path.resolve(this.storedCWD, pluginPath) + args;
}
- plugin += utilities.PARAMEDIC_COMMON_CLI_ARGS +
utilities.PARAMEDIC_PLUGIN_ADD_ARGS;
- logger.normal('cordova-paramedic: installing plugin ' + plugin);
+ const results = await spawnAsync(
+ this.config.getCli(),
+ ['plugin', 'add', plugin, ...utilities.PARAMEDIC_COMMON_ARGS],
+ { cwd: this.appRoot }
+ );
- const plugAddCmd = exec(this.config.getCli() + ' plugin add ' +
plugin);
- if (plugAddCmd.code !== 0) {
- logger.error('Failed to install plugin : ' + plugin);
- throw new Error('Failed to install plugin : ' + plugin);
+ if (results.code !== 0) {
+ logger.error(`[paramedic] Failed to install plugin: ${plugin}`);
+ throw new Error(`[paramedic] Failed to install plugin: ${plugin}`);
}
}
- showPluginsVersions () {
- logger.normal('cordova-paramedic: versions of installed plugins: ');
- exec(this.config.getCli() + ' plugins' +
utilities.PARAMEDIC_COMMON_CLI_ARGS);
+ /**
+ * Fetches and displays list of all installed plugins.
+ */
+ async showPluginsVersions () {
+ const results = await spawnAsync(
+ this.config.getCli(),
+ ['plugins', ...utilities.PARAMEDIC_COMMON_ARGS],
+ { cwd: this.appRoot }
+ );
+
+ logger.normal(results.stdout);
}
}
diff --git a/lib/paramedic.js b/lib/paramedic.js
index 43d549f..630d99d 100644
--- a/lib/paramedic.js
+++ b/lib/paramedic.js
@@ -17,12 +17,12 @@
under the License.
*/
-const cp = require('child_process');
-const shell = require('shelljs');
const Server = require('./LocalServer');
const path = require('path');
const fs = require('fs');
-const { logger, exec, execPromise, utilities } = require('./utils');
+const { setTimeout: timelimit } = require('node:timers/promises');
+
+const { logger, utilities, spawnAsync } = require('./utils');
const Reporters = require('./Reporters');
const ParamedicKill = require('./ParamedicKill');
const ParamedicLogCollector = require('./ParamedicLogCollector');
@@ -43,80 +43,84 @@ class ParamedicRunner {
this.isBrowser = this.config.getPlatformId() === utilities.BROWSER;
this.isIos = this.config.getPlatformId() === utilities.IOS;
-
- exec.setVerboseLevel(config.isVerbose());
}
- run () {
+ /**
+ * The main runner that:
+ * - Creates, sets up, & prepares the project.
+ * - Runs the project
+ * - Executes the tests
+ *
+ * On a successful case, the test results should be returned.
+ *
+ * An error can be thrown if there was any issues within the
+ * process. Failures in the app uninstall process will not
+ * error out.
+ *
+ * @returns {Promise}
+ */
+ async run () {
this.checkConfig();
- return Promise.resolve()
- .then(() => {
- // create project and prepare (install plugins, setup test
startpage, install platform, check platform requirements)
- const paramedicApp = new ParamedicApp(this.config,
this.storedCWD, this);
- this.tempFolder = paramedicApp.createTempProject();
- shell.pushd(this.tempFolder.name);
- return paramedicApp.prepareProjectToRunTests();
- })
- .then(() => {
- if (this.config.runMainTests()) {
- // start server
- const noListener = false;
- return Server.startServer(this.config.getPorts(),
noListener);
- }
- })
- .then((server) => {
- if (this.config.runMainTests()) {
- // configure server usage
- this.server = server;
+ const paramedicApp = new ParamedicApp(this.config, this.storedCWD,
this);
- this.injectReporters();
- this.subcribeForEvents();
+ try {
+ // Create a Cordova project
+ this.tempFolder = await paramedicApp.createTempProject();
- const logUrl =
this.server.getMedicAddress(this.config.getPlatformId());
- this.writeMedicJson(logUrl);
+ // Prepare the project by installing plugins, platforms, seting up
test startpage, & check platform requirements
+ await paramedicApp.prepareProjectToRunTests();
- logger.normal('Start building app and running tests at ' +
(new Date()).toLocaleTimeString());
- }
- // run tests
- return Promise.race([
- this.runTests(),
- new Promise((resolve, reject) =>
- setTimeout(() => reject(
- new Error(`[paramedic] Tests failed to complete in
${this.config.getTimeout()} ms.`)
- ), this.config.getTimeout())
- )
- ]);
- })
- .catch((error) => {
- logger.error(error);
- console.log(error.stack);
- throw error;
- })
- .then((result) => {
-
logger.warn('---------------------------------------------------------');
- logger.warn('6. Collect data and clean up');
-
logger.warn('---------------------------------------------------------');
- logger.normal('Completed tests at ' + (new
Date()).toLocaleTimeString());
-
- // When --justbuild is not set, fetch logs from the device.
- if (this.config.getAction() !== 'build') {
- // collect logs and uninstall app
- this.collectDeviceLogs();
- return this.uninstallApp()
- .catch(() => { /* do not fail if uninstall failed */ })
- .finally(() => {
- this.killEmulatorProcess();
- })
- .then(() => result);
+ // Start server if the tests are to run
+ if (this.config.runMainTests()) {
+ const noListener = false;
+ this.server = await Server.startServer(this.config.getPorts(),
noListener);
+
+ this.injectReporters();
+ this.subcribeForEvents();
+
+ const logUrl =
this.server.getMedicAddress(this.config.getPlatformId());
+ this.writeMedicJson(logUrl);
+
+ logger.normal('[paramedic] Start building app and running
tests at ' + (new Date()).toLocaleTimeString());
+ }
+
+ const results = await Promise.race([
+ this.runLocalTests(),
+ // If the tests fails to complete in the allowed timelimit, it
will reject (default 60 minutes)
+ timelimit(this.config.getTimeout())
+ .then(() => Promise.reject(
+ new Error(`[paramedic] Tests failed to complete in
${this.config.getTimeout()} ms.`)
+ ))
+ ]);
+
+
logger.warn('---------------------------------------------------------');
+ logger.warn('6. Collect data and clean up');
+
logger.warn('---------------------------------------------------------');
+ logger.normal('Completed tests at ' + (new
Date()).toLocaleTimeString());
+
+ // When --justbuild is not set, fetch logs from the device.
+ if (this.config.getAction() !== 'build') {
+ // collect logs and uninstall app
+ await this.collectDeviceLogs();
+
+ try {
+ await this.uninstallApp();
+ } catch {
+ // do not fail if uninstall failed
+ } finally {
+ this.killEmulatorProcess();
}
+ }
- // --justbuild does nothing.
- return result;
- })
- .finally(() => {
- this.cleanUpProject();
- });
+ return results;
+ } catch (error) {
+ logger.error(error);
+ console.log(error.stack);
+ throw error;
+ } finally {
+ this.cleanUpProject();
+ }
}
checkConfig () {
@@ -143,18 +147,21 @@ class ParamedicRunner {
}
}
- logger.info('cordova-paramedic: Will use the following cli: ' +
this.config.getCli());
+ logger.info('[paramedic] Will use the following cli: ' +
this.config.getCli());
}
- setPermissions () {
+ /**
+ * Setup iOS related Permissions
+ */
+ async setPermissions () {
const applicationsToGrantPermission = ['kTCCServiceAddressBook'];
if (this.isIos) {
- logger.info('cordova-paramedic: Setting required permissions.');
+ logger.info('[paramedic] Setting required permissions.');
const tccDb = this.config.getTccDb();
if (tccDb) {
const appName = utilities.PARAMEDIC_DEFAULT_APP_NAME;
const paramediciOSPermissions = new
ParamediciOSPermissions(appName, tccDb, this.targetObj);
-
paramediciOSPermissions.updatePermissions(applicationsToGrantPermission);
+ await
paramediciOSPermissions.updatePermissions(applicationsToGrantPermission);
}
}
}
@@ -184,23 +191,29 @@ class ParamedicRunner {
});
this.server.on('deviceInfo', (data) => {
- logger.normal('cordova-paramedic: Device info: ' +
JSON.stringify(data));
+ logger.normal('[paramedic] Device info: ' + JSON.stringify(data));
});
}
writeMedicJson (logUrl) {
- logger.normal('cordova-paramedic: writing medic log url to project ' +
logUrl);
+ logger.normal('[paramedic] writing medic log url to project ' +
logUrl);
const medicFilePath = path.join(this.tempFolder.name, 'www',
'medic.json');
const medicFileContent = JSON.stringify({ logurl: logUrl });
fs.writeFileSync(medicFilePath, medicFileContent);
}
- runLocalTests () {
+ /**
+ * Runs the local tests (Jasmine) and returns the results.
+ * A reject maybe returned for example the tests do not complete in the
timelimit.
+ *
+ * @returns {Promise}
+ */
+ async runLocalTests () {
+
logger.warn('---------------------------------------------------------');
+ logger.warn('4. Run (Jasmine) tests...');
logger.warn('... locally');
logger.warn('---------------------------------------------------------');
- let runProcess = null;
-
// checking for Android platform here because in this case we still
need to start an emulator
// will check again a bit lower
if (!this.config.runMainTests() && this.config.getPlatformId() !==
utilities.ANDROID) {
@@ -208,122 +221,96 @@ class ParamedicRunner {
return utilities.TEST_PASSED;
}
- logger.info('cordova-paramedic: running tests locally');
-
- return Promise.resolve()
- .then(() => this.getCommandForStartingTests())
- .then((command) => {
- this.setPermissions();
-
- return Promise.all([
- Promise.resolve().then(() => {
- logger.normal('cordova-paramedic: running command ' +
command);
-
- if (this.config.getPlatformId() !== utilities.BROWSER)
{
- return execPromise(command);
- }
- console.log('$ ' + command);
-
- // a precaution not to try to kill some other process
- runProcess = cp.exec(command, () => {
- runProcess = null;
- });
- }),
- Promise.resolve().then(() => {
- if (!this.config.runMainTests()) {
- logger.normal('Skipping main tests...');
- return utilities.TEST_PASSED;
- }
-
- // skip tests if it was just build
- if (this.shouldWaitForTestResult()) {
- return new Promise((resolve, reject) => {
- // reject if timed out
- this.waitForConnection().catch(reject);
- // resolve if got results
- this.waitForTests().then(resolve);
- });
- }
-
- return utilities.TEST_PASSED; // if we're not waiting
for a test result, just report tests as passed
- })
- ]);
- })
- .then((results) => results[1])
- .then((result) => {
- if (runProcess) {
- return new Promise((resolve) => {
- utilities.killProcess(runProcess.pid, () => {
- resolve(result);
- });
- });
- }
+ logger.info('[paramedic] running tests locally');
+ await this.setPermissions();
- return result;
- });
- }
+ const cmdArgs = await this.getRunLocalTestCommandArgs();
- runTests () {
-
logger.warn('---------------------------------------------------------');
- logger.warn('4. Run (Jasmine) tests...');
- return this.runLocalTests();
+ if (this.config.getAction() === 'build') {
+ await spawnAsync(
+ this.config.getCli(),
+ cmdArgs,
+ { cwd: this.tempFolder.name }
+ );
+
+ // Build only does not trigger tests. Pass will be returned.
+ return utilities.TEST_PASSED;
+ }
+
+ // Main tests are being skipped. Pass will be returned.
+ if (!this.config.runMainTests()) {
+ logger.normal('[paramedic] Skipping main tests...');
+ return utilities.TEST_PASSED;
+ }
+
+ // Waiting for test results for run/emulate commands.
+ if (this.shouldWaitForTestResult()) {
+ return await Promise.race([
+ this.waitForTests(cmdArgs), // resolve on request
+ timelimit(INITIAL_CONNECTION_TIMEOUT).then(() => {
+ if (!this.server.isDeviceConnected()) {
+ const ERR_MSG = `[paramedic] The device failed to
connect to local server in ${INITIAL_CONNECTION_TIMEOUT / 1000} secs`;
+ return Promise.reject(new Error(ERR_MSG));
+ }
+ })
+ ]);
+ }
+
+ // Nothing happened so return pass.
+ return utilities.TEST_PASSED;
}
- waitForTests () {
- logger.info('cordova-paramedic: waiting for test results');
- return new Promise((resolve, reject) => {
- // time out if connection takes too long
- const ERR_MSG = 'waitForTests: Seems like device not connected to
local server in ' + INITIAL_CONNECTION_TIMEOUT / 1000 + ' secs';
- setTimeout(() => {
- if (!this.server.isDeviceConnected()) {
- reject(new Error(ERR_MSG));
- }
- }, INITIAL_CONNECTION_TIMEOUT);
+ async waitForTests (cmdArgs) {
+ logger.info('[paramedic] Waiting for test results...');
+ const testResults = new Promise((resolve, reject) => {
this.server.on('jasmineDone', (data) => {
- logger.info('cordova-paramedic: tests have been completed');
-
- // Is Test Passed
- resolve((data.specResults.specFailed === 0));
+ logger.info('[paramedic] Tests has completed.');
+ resolve(data.specResults.specFailed === 0);
});
-
this.server.on('disconnect', () => {
- reject(new Error('Device is disconnected before passing the
tests'));
+ reject(new Error('[paramedic] Device is disconnected before
passing the tests'));
});
});
- }
- getCommandForStartingTests () {
- const cmd = [
+ // This spawns the Cordova run command. It will build, run, and
trigger the automatic tests.
+ await spawnAsync(
this.config.getCli(),
+ cmdArgs,
+ { cwd: this.tempFolder.name }
+ );
+
+ return testResults;
+ }
+
+ /**
+ * Creates the run/build command.
+ *
+ * @returns {Array}
+ */
+ async getRunLocalTestCommandArgs () {
+ const args = [
this.config.getAction(),
- this.config.getPlatformId()
- ]
- .concat(utilities.PARAMEDIC_COMMON_ARGS)
- .concat([this.config.getArgs()]);
-
- if (this.isBrowser) {
- return cmd.join(' ');
- } else if (this.config.getAction() === 'build') {
- // The app is to be run as a store app or just build. So no need
to choose a target.
- return Promise.resolve(cmd.join(' '));
+ this.config.getPlatformId(),
+ ...this.config.getArgs(),
+ ...utilities.PARAMEDIC_COMMON_ARGS
+ ];
+
+ if (this.isBrowser || this.config.getAction() === 'build') {
+ return args;
}
- // For now we always trying to run test app on emulator
- return new ParamedicTargetChooser(this.tempFolder.name,
this.config).chooseTarget(
- true, // useEmulator
- this.config.getTarget() // preferredTarget
- ).then(targetObj => {
- this.targetObj = targetObj;
-
- return cmd
- .concat(['--target', `"${this.targetObj.target}"`])
- // CB-11472 In case of iOS provide additional '--emulator'
flag, otherwise
- // 'cordova run ios --target' would hang waiting for device
with name
- // as specified in 'target' in case if any device is
physically connected
- .concat(this.isIos ? ['--emulator'] : [])
- .join(' ');
- });
+ const targetChooser = new ParamedicTargetChooser(this.tempFolder.name,
this.config);
+ this.targetObj = await
targetChooser.chooseTarget(this.config.getTarget());
+
+ // CB-11472 In case of iOS provide additional '--emulator' flag,
otherwise
+ // 'cordova run ios --target' would hang waiting for device with name
+ // as specified in 'target' in case if any device is physically
connected
+ return [
+ ...args,
+ '--target',
+ this.targetObj.target
+ ].concat(this.isIos ? ['--emulator'] : []);
}
shouldWaitForTestResult () {
@@ -331,45 +318,36 @@ class ParamedicRunner {
return (action.indexOf('run') === 0) || (action.indexOf('emulate') ===
0);
}
- waitForConnection () {
- const ERR_MSG = 'waitForConnection: Seems like device not connected to
local server in ' + INITIAL_CONNECTION_TIMEOUT / 1000 + ' secs';
-
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- if (!this.server.isDeviceConnected()) {
- reject(new Error(ERR_MSG));
- } else {
- resolve();
- }
- }, INITIAL_CONNECTION_TIMEOUT);
- });
- }
-
+ /**
+ * Removes the temporary project directory if flagged to cleanup after run.
+ */
cleanUpProject () {
if (this.config.shouldCleanUpAfterRun()) {
- logger.info('cordova-paramedic: Deleting the application: ' +
this.tempFolder.name);
- shell.popd();
- shell.rm('-rf', this.tempFolder.name);
+ logger.info('[paramedic] Removing Temporary Project: ' +
this.tempFolder.name);
+ fs.rmSync(this.tempFolder.name, { force: true, recursive: true });
}
}
killEmulatorProcess () {
if (this.config.shouldCleanUpAfterRun()) {
- logger.info('cordova-paramedic: Killing the emulator process.');
+ logger.info('[paramedic] Terminating Emulator Process');
const paramedicKill = new
ParamedicKill(this.config.getPlatformId());
paramedicKill.kill();
}
}
- collectDeviceLogs () {
- logger.info('Collecting logs for the devices.');
+ /**
+ * Collects and stores logs when possible
+ */
+ async collectDeviceLogs () {
+ logger.info('[paramedic] Collecting Device Logs');
const outputDir = this.config.getOutputDir() ?
this.config.getOutputDir() : this.tempFolder.name;
const paramedicLogCollector = new
ParamedicLogCollector(this.config.getPlatformId(), this.tempFolder.name,
outputDir, this.targetObj);
- paramedicLogCollector.collectLogs();
+ await paramedicLogCollector.collectLogs();
}
uninstallApp () {
- logger.info('Uninstalling the app.');
+ logger.info('[paramedic] Uninstalling App');
const paramedicAppUninstall = new
ParamedicAppUninstall(this.tempFolder.name, this.config.getPlatformId());
return paramedicAppUninstall.uninstallApp(this.targetObj,
utilities.PARAMEDIC_DEFAULT_APP_NAME);
}
diff --git a/lib/utils/execWrapper.js b/lib/utils/execWrapper.js
deleted file mode 100644
index 9f29b64..0000000
--- a/lib/utils/execWrapper.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
-*/
-
-const shelljs = require('shelljs');
-let verbose;
-
-function exec (cmd, onFinish, onData) {
- console.log('$ ' + cmd);
- if (onFinish instanceof Function || onFinish === null) {
- const result = shelljs.exec(cmd, { async: true, silent: !verbose },
onFinish);
-
- if (onData instanceof Function) {
- result.stdout.on('data', onData);
- }
- } else {
- return shelljs.exec(cmd, { silent: !verbose });
- }
-}
-
-function execPromise (cmd) {
- return new Promise(function (resolve, reject) {
- exec(cmd, function (code, output) {
- if (code) {
- reject(output);
- } else {
- resolve(output);
- }
- });
- });
-}
-
-exec.setVerboseLevel = function (_verbose) {
- verbose = _verbose;
-};
-
-module.exports.exec = exec;
-module.exports.execPromise = execPromise;
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e4949cc..978ded4 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -18,8 +18,7 @@
*/
module.exports = {
- exec: require('./execWrapper').exec,
logger: require('cordova-common').CordovaLogger.get(),
- execPromise: require('./execWrapper').execPromise,
- utilities: require('./utilities')
+ utilities: require('./utilities'),
+ ...require('./spawn')
};
diff --git a/lib/utils/spawn.js b/lib/utils/spawn.js
new file mode 100644
index 0000000..2b0f063
--- /dev/null
+++ b/lib/utils/spawn.js
@@ -0,0 +1,136 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+const { spawn } = require('node:child_process');
+const { setTimeout: timelimit } = require('node:timers/promises');
+
+const logger = require('cordova-common').CordovaLogger.get();
+
+/**
+ * Wraps the spawn process inside a promise to make it asynchronous.
+ *
+ * In a successful case, during the on close event, an object will be
+ * returned as long as the code equals 0.
+ * The object will contain the process's "stdout", "stderr", & "code".
+ *
+ * If the code is anything but zero, a error object is returned as
+ * a rejection. The "stdout", "stderr", & "code" will be appeneded.
+ *
+ * An error is also returned in cases where the process is errored
+ * but not closed.
+ *
+ * The "options" argument is used for configuring the spawn process.
+ * It also takes in the "verbose" and "timeout" settings. These
+ * settings will be extracted from options before passed to the spawn
+ * process.
+ *
+ * "verbose" - Determins the level of logging.
+ * "timeout" - Determins if the spawn should timeout after Xms.
+ * Value should be set in milliseconds.
+ *
+ * @param {String} cmd command process (e.g. cordova, adb, xcrun, etc...)
+ * @param {Array} args command arguments
+ * @param {Object} options Spawn process object, verbose, and timeout settings
+ * @returns {Promise<Object|Error>}
+ */
+async function spawnAsync (cmd, args = [], options = {}) {
+ // Seperate non-spawn and spawn options.
+ const { verbose = false, timeout = false, ...spawnOptions } = options;
+
+ // Tracking start and stop time to attach with the last log message how
long
+ // a given process took in milliseconds.
+ const timeStart = new Date();
+
+ if (verbose) {
+ logger.info(`[paramedic] Running command: ${cmd} ${args.join(' ')}`);
+ }
+
+ // Stores the spawn process that can be killed by the timeout limit if set
and reached.
+ let proc = null;
+ // Contians a collection of promises that will be pushed to the races.
+ const promises = [];
+
+ promises.push(
+ // The main promise that wraps the spawn.
+ new Promise((resolve, reject) => {
+ proc = spawn(cmd, args, { stdio: 'pipe', ...spawnOptions });
+
+ let stdout = '';
+ let stderr = '';
+
+ if (proc.stdout) {
+ proc.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+ }
+
+ if (proc.stderr) {
+ proc.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+ }
+
+ proc.on('error', (err) => {
+ reject(err);
+ });
+
+ proc.on('close', (code) => {
+ // This is the time difference in milliseconds of how long the
process took.
+ const timeDiff = new Date() - timeStart;
+
+ if (code === 0) {
+ if (verbose) {
+ logger.info(`[paramedic] Finished running command:
"${cmd} ${args.join(' ')}" in ${timeDiff}ms.`);
+ }
+ // Collect the output & code to return back.
+ resolve({ stdout, stderr, code });
+ } else {
+ // If the code was not 0, we will reject the promise.
+ // As a rejection takes in an "Error" object, we will
append the stdout, stderr, and code
+ // so it will be availble as well. This is an extension
and not normal pattern.
+ const error = new Error(
+ `[paramedic] Command failed: "${cmd} ${args.join('
')}" in ${timeDiff}ms.\nExit Code: ${code} & Message: \n${stderr}`
+ );
+ error.stdout = stdout;
+ error.stderr = stderr;
+ error.code = code;
+ reject(error);
+ }
+ });
+ })
+ );
+
+ // When timout is set correctly, we will push to the promises array a
timeout promise.
+ // If this timeout finishes before the spawn process finishes, we will
kill spawn process,
+ // null it out, and return a rejection that the command timed out.
+ if (typeof timeout === 'number' && timeout > 0) {
+ promises.push(
+ timelimit(timeout).then(() => {
+ proc?.kill();
+ proc = null;
+ Promise.reject(new Error(`[paramedic] Command timed out after
${timeout}ms`));
+ })
+ );
+ }
+
+ // Off to the races
+ return Promise.race([...promises]);
+}
+
+module.exports = { spawnAsync };
diff --git a/lib/utils/utilities.js b/lib/utils/utilities.js
index 524dd9c..4959129 100644
--- a/lib/utils/utilities.js
+++ b/lib/utils/utilities.js
@@ -19,17 +19,14 @@
under the License.
*/
-const fs = require('fs');
-const os = require('os');
-const util = require('util');
-const path = require('path');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+
const logger = require('cordova-common').CordovaLogger.get();
const kill = require('tree-kill');
-const exec = require('./execWrapper').exec;
-const execa = require('execa');
+const { spawnAsync } = require('./spawn');
-const HEADING_LINE_PATTERN = /List of devices/m;
-const DEVICE_ROW_PATTERN = /(emulator|device|host)/m;
const KILL_SIGNAL = 'SIGINT';
let simulatorCollection = null;
@@ -39,23 +36,6 @@ function isWindows () {
return /^win/.test(os.platform());
}
-function countAndroidDevices () {
- const listCommand = 'adb devices';
-
- logger.info('running:');
- logger.info(' ' + listCommand);
-
- let numDevices = 0;
- const result = exec(listCommand);
- result.stdout.split('\n').forEach(function (line) {
- if (!HEADING_LINE_PATTERN.test(line) && DEVICE_ROW_PATTERN.test(line))
{
- numDevices += 1;
- }
- });
-
- return numDevices;
-}
-
function secToMin (seconds) {
return Math.ceil(seconds / 60);
}
@@ -64,7 +44,7 @@ function getSimulatorsFolder () {
return path.join(os.homedir(), 'Library', 'Developer', 'CoreSimulator',
'Devices');
}
-function getSimulatorModelId (cli, target) {
+async function getSimulatorModelId (appPath, cli, target) {
target = new RegExp(target || '^iPhone');
const args = [
@@ -74,13 +54,13 @@ function getSimulatorModelId (cli, target) {
'--emulator'
].concat(module.exports.PARAMEDIC_COMMON_ARGS);
- // Fetches all known simulators/emulators.
- logger.info('running:');
- logger.info(` ${cli} ${args.join(' ')}`);
+ const result = await spawnAsync(
+ cli,
+ args,
+ { cwd: appPath }
+ );
- const result = execa.sync(cli, args);
-
- if (result.exitCode > 0) {
+ if (result.code > 0) {
logger.error('Failed to find simulator we deployed to');
return;
}
@@ -101,22 +81,24 @@ function getSimulatorModelId (cli, target) {
.trim();
}
-function getSimulatorCollection () {
- if (simulatorCollection) return simulatorCollection;
-
- // Next, figure out the ID of the simulator we found
- const cmd = '(xcrun xctrace list devices || instruments -s devices) 2>&1 |
grep ^iPhone';
- logger.info('running:');
- logger.info(' ' + cmd);
+/**
+ * Attempts to fetch and return an array of iPhone simulators from xcrun
xctrace.
+ *
+ * @returns {Array|Boolean}
+ */
+async function getSimulatorCollection () {
+ if (simulatorCollection) {
+ return simulatorCollection;
+ }
- const cmdResult = exec(cmd);
+ const devices = await spawnAsync('xcrun', ['xctrace', 'list', 'devices']);
- if (cmdResult.code > 0) {
- logger.error('Failed to get the list of simulators');
+ if (devices.code !== 0) {
+ logger.error('[paramedic] Failed to fetch simulator list.');
return false;
}
- simulatorCollection = cmdResult.stdout.split('\n');
+ simulatorCollection = devices.stdout.split('\n').filter(line =>
line.startsWith('iPhone'));
return simulatorCollection;
}
@@ -142,7 +124,7 @@ function filterForSimulatorIds (simulatorData,
simulatorCollection) {
}, []);
}
-function getSimulatorData (findSimResult) {
+async function getSimulatorData (findSimResult) {
if (simulatorDataCollection[findSimResult]) return
simulatorDataCollection[findSimResult];
// Format of the output is "iPhone-6s-Plus, 9.1"
@@ -156,7 +138,7 @@ function getSimulatorData (findSimResult) {
};
// Fetch the environment's installed simulator collection data
- const simulators = getSimulatorCollection();
+ const simulators = await getSimulatorCollection();
// Try to find the simulator ids from the simulator collection
const simulatorIds = filterForSimulatorIds(simulatorData, simulators);
@@ -199,12 +181,6 @@ function mkdirSync (path) {
}
}
-function getSqlite3InsertionCommand (destinationTCCFile, service, appName) {
- return util.format('sqlite3 %s "insert into access' +
- '(service, client, client_type, allowed, prompt_count, csreq)
values(\'%s\', \'%s\', ' +
- '0,1,1,NULL)"', destinationTCCFile, service, appName);
-}
-
function contains (collection, item) {
return collection.indexOf(item) !== (-1);
}
@@ -244,11 +220,8 @@ module.exports = {
IOS: 'ios',
BROWSER: 'browser',
PARAMEDIC_DEFAULT_APP_NAME: 'io.cordova.hellocordova',
- PARAMEDIC_COMMON_CLI_ARGS: ' --no-telemetry --no-update-notifier',
PARAMEDIC_COMMON_ARGS: ['--no-telemetry', '--no-update-notifier'],
- PARAMEDIC_PLUGIN_ADD_ARGS: '',
- PARAMEDIC_PLATFORM_ADD_ARGS: '',
- DEFAULT_ENCODING: 'utf-8',
+ DEFAULT_ENCODING: 'utf8',
DEFAULT_LOG_TIME: 15,
DEFAULT_LOG_TIME_ADDITIONAL: 2,
@@ -257,10 +230,8 @@ module.exports = {
secToMin,
isWindows,
- countAndroidDevices,
getSimulatorsFolder,
doesFileExist,
- getSqlite3InsertionCommand,
getSimulatorModelId,
getSimulatorData,
contains,
diff --git a/package-lock.json b/package-lock.json
index a43fa57..dc14a95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,11 +10,9 @@
"license": "Apache-2.0",
"dependencies": {
"cordova-common": "^6.0.0",
- "execa": "^5.1.1",
"jasmine-reporters": "^2.5.2",
"jasmine-spec-reporter": "^7.0.0",
"minimist": "^1.2.8",
- "shelljs": "^0.10.0",
"tcp-port-used": "^1.0.2",
"tmp": "^0.2.5",
"tree-kill": "^1.2.2",
@@ -771,6 +769,7 @@
"version": "7.0.6",
"resolved":
"https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity":
"sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -1499,29 +1498,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/execa": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
- "integrity":
"sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^6.0.0",
- "human-signals": "^2.1.0",
- "is-stream": "^2.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^4.0.1",
- "onetime": "^5.1.2",
- "signal-exit": "^3.0.3",
- "strip-final-newline": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
"node_modules/extsprintf": {
"version": "1.4.1",
"resolved":
"https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
@@ -1764,18 +1740,6 @@
"node": ">= 0.4"
}
},
- "node_modules/get-stream": {
- "version": "6.0.1",
- "resolved":
"https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity":
"sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved":
"https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -1971,15 +1935,6 @@
"node": ">= 0.4"
}
},
- "node_modules/human-signals": {
- "version": "2.1.0",
- "resolved":
"https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
- "integrity":
"sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=10.17.0"
- }
- },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2333,18 +2288,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-stream": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
- "integrity":
"sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-string": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
@@ -2473,6 +2416,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity":
"sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
"license": "ISC"
},
"node_modules/jasmine-reporters": {
@@ -2604,12 +2548,6 @@
"node": ">= 0.4"
}
},
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved":
"https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity":
"sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "license": "MIT"
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2632,15 +2570,6 @@
"node": ">=8.6"
}
},
- "node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity":
"sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2689,18 +2618,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/npm-run-path": {
- "version": "4.0.1",
- "resolved":
"https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
- "integrity":
"sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved":
"https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -2804,21 +2721,6 @@
"integrity":
"sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==",
"license": "ISC"
},
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity":
"sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/optionator": {
"version": "0.9.4",
"resolved":
"https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2914,6 +2816,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity":
"sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -3244,6 +3147,7 @@
"version": "2.0.0",
"resolved":
"https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity":
"sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -3256,24 +3160,12 @@
"version": "3.0.0",
"resolved":
"https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity":
"sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
- "node_modules/shelljs": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz",
- "integrity":
"sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "execa": "^5.1.1",
- "fast-glob": "^3.3.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/side-channel": {
"version": "1.1.0",
"resolved":
"https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -3350,12 +3242,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved":
"https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity":
"sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "license": "ISC"
- },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved":
"https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -3439,15 +3325,6 @@
"node": ">=4"
}
},
- "node_modules/strip-final-newline": {
- "version": "2.0.0",
- "resolved":
"https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
- "integrity":
"sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved":
"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3752,6 +3629,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity":
"sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
diff --git a/package.json b/package.json
index ced1201..73b5773 100644
--- a/package.json
+++ b/package.json
@@ -31,11 +31,9 @@
"author": "Apache Software Foundation",
"dependencies": {
"cordova-common": "^6.0.0",
- "execa": "^5.1.1",
"jasmine-reporters": "^2.5.2",
"jasmine-spec-reporter": "^7.0.0",
"minimist": "^1.2.8",
- "shelljs": "^0.10.0",
"tcp-port-used": "^1.0.2",
"tmp": "^0.2.5",
"tree-kill": "^1.2.2",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]