chore!(base-driver): remove support for execute driver script since it will be a plugin moving forward
MODIFY
docs/en/advanced-concepts/migrating-to-appium-2.0.md
docs/en/advanced-concepts/migrating-to-appium-2.0.md
@@ -129,6 +129,13 @@ If you use these image-related methods, to continue accessing them you will need
Image-related commands will also be removed on the client side of things, which means you will need to follow the instructions on the plugin README for installing client-side plugins to access these features.
+### :bangbang: Execute Driver Script command moved to plugin
+
+If you use the advanced Execute Driver Script feature (which allows you to send in a WebdriverIO script to have it executed completely on the server instead of command-by-command from the client), this functionality has been moved to a plugin. Here's what to do to keep using it:
+
+1. Install the plugin: `appium plugin install execute-driver`
+2. Ensure you start the Appium server with access to run the plugin by including it in the list of plugins designated on the command line, e.g., `appium --use-plugins=execute-driver`
+
### :bangbang: Old drivers removed
The old iOS and Android (UiAutomator 1) drivers and related tools (e.g., `authorize-ios`) have been removed. They haven't been relevant for many years anyway.

MODIFY
packages/base-driver/index.d.ts
packages/base-driver/index.d.ts
@@ -173,7 +173,6 @@ declare class BaseDriver {
updateSettings(newSettings: Record<string, unknown>): Promise<void>;
getSettings(): Promise<Record<string, unknown>>;
receiveAsyncResponse?(response: unknown): Promise<void>;
- executeDriverScript(script: string, scriptType?: string, timeout?: number): Promise<unknown>;
getLogEvents(type?: string | string[]): {};
logCustomEvent(vendor: string, event: string): void;
setClipboard?(content: string, contentType?: string, label?: string): Promise<void>;

DELETE
packages/base-driver/lib/basedriver/commands/execute-child.js
-import _ from 'lodash';
-import B from 'bluebird';
-import vm from 'vm';
-import log from '../logger';
-import { attach } from 'webdriverio';
-
-// duplicate defining these keys here so we don't need to re-load a huge appium
-// dependency tree into memory just to run a wdio script
-const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
-const MJSONWP_ELEMENT_KEY = 'ELEMENT';
-
-async function runScript (driverOpts, script, timeout) {
- if (!_.isNumber(timeout)) {
- throw new Error('Timeout parameter must be a number');
- }
-
- // set up fake logger
- const logLevels = ['error', 'warn', 'log'];
- const logs = {};
- const consoleFns = {};
- for (const level of logLevels) {
- logs[level] = [];
- consoleFns[level] = (...logMsgs) => logs[level].push(...logMsgs);
- }
-
- const driver = attach(driverOpts);
-
- const fullScript = buildScript(script);
- // the timeout here will not matter really, but set it anyway to be on the
- // safe side
- const vmCtx = vm.runInNewContext(fullScript, {}, {timeout});
-
- // run the driver script, giving user access to the driver object, a fake
- // console logger, and a promise library
- log.info('Running driver script in Node vm');
- let result = await vmCtx(driver, consoleFns, B);
-
- log.info('Ensuring driver script result is appropriate type for return');
- result = coerceScriptResult(result);
- return {result, logs};
-}
-
-/**
- * Embed a user-generated script inside a method which takes only the
- * predetermined objects we specify
- *
- * @param {string} script - the javascript to execute
- *
- * @return {string} - the full script to execute
- */
-function buildScript (script) {
- return `(async function execute (driver, console, Promise) {
- ${script}
- })`;
-}
-
-/**
- * We can get any manner of crazy thing back from a vm executing untrusted
- * code. We might also get WebdriverIO objects that aren't suitable for JSON
- * response. So make sure we convert the things we know about to their
- * appropriate response format, and squash other weird things.
- *
- * @param {Object} obj - object to convert and sanitize
- *
- * @return {Object} - safely converted object
- */
-function coerceScriptResult (obj) {
- // first ensure obj is of a type that can be JSON encoded safely. This will
- // get rid of custom objects, functions, etc... and turn them into POJOs
- try {
- obj = JSON.parse(JSON.stringify(obj));
- } catch (e) {
- log.warn('Could not convert executeDriverScript to safe response!' +
- `Result was: ${obj}. Will make it null`);
- return null;
- }
-
- let res;
-
- // now we begin our recursive case options
- if (_.isPlainObject(obj)) {
- // if we have an object, it's either an element object or something else
- // webdriverio has no monadic object types other than element and driver,
- // and we don't want to allow special casing return of driver
- res = {};
-
- if (obj[MJSONWP_ELEMENT_KEY] || obj[W3C_ELEMENT_KEY]) {
- // if it's an element object, clear out anything that's not the key, and
- // then return the object
- if (obj[MJSONWP_ELEMENT_KEY]) {
- res[MJSONWP_ELEMENT_KEY] = obj[MJSONWP_ELEMENT_KEY];
- }
-
- if (obj[W3C_ELEMENT_KEY]) {
- res[W3C_ELEMENT_KEY] = obj[W3C_ELEMENT_KEY];
- }
- return res;
- }
-
- // otherwise, recurse into the object
- for (const key of Object.keys(obj)) {
- res[key] = coerceScriptResult(obj[key]);
- }
- return res;
- }
-
- // in the cae of an array, just recurse into the items
- if (_.isArray(obj)) {
- return obj.map(coerceScriptResult);
- }
-
- // base case, if it's not an object or array, return straightaway
- return obj;
-}
-
-async function main (driverOpts, script, timeout) {
- let res;
- try {
- res = {success: await runScript(driverOpts, script, timeout)};
- } catch (error) {
- res = {error: {message: error.message, stack: error.stack}};
- }
- await B.promisify(process.send, {context: process})(res);
-}
-
-if (require.main === module) {
- log.info('Running driver execution in child process');
- process.on('message', ({driverOpts, script, timeout}) => {
- log.info('Parameters received from parent process');
- main(driverOpts, script, timeout);
- });
-}

DELETE
packages/base-driver/lib/basedriver/commands/execute.js
-import _ from 'lodash';
-import path from 'path';
-import cp from 'child_process';
-import log from '../logger';
-import B from 'bluebird';
-
-const FEAT_FLAG = 'execute_driver_script';
-const DEFAULT_SCRIPT_TIMEOUT = 1000 * 60 * 60; // default to 1 hour timeout
-const SCRIPT_TYPE_WDIO = 'webdriverio';
-// TODO add wd script type at some point
-
-let commands = {};
-
-/**
- * This method takes a string which is executed as javascript in the context of
- * a new nodejs VM, and which has available a webdriverio driver object, having
- * already been attached to the currently running session.
- *
- * @param {string} script - the string representing the driver script to run
- * @param {string} [scriptType='webdriverio'] - the name of the driver script
- * library (currently only webdriverio is supported)
- *
- * @returns {Object} - a JSONifiable object representing the return value of
- * the script
- */
-commands.executeDriverScript = async function (script, scriptType = 'webdriverio',
- timeout = DEFAULT_SCRIPT_TIMEOUT) {
-
- if (!this.isFeatureEnabled(FEAT_FLAG)) {
- throw new Error(`Execute driver script functionality is not available ` +
- `unless server is started with --allow-insecure including ` +
- `the '${FEAT_FLAG}' flag, e.g., --allow-insecure=${FEAT_FLAG}`);
- }
-
- if (scriptType !== SCRIPT_TYPE_WDIO) {
- throw new Error(`Only the '${SCRIPT_TYPE_WDIO}' script type is currently supported`);
- }
-
- if (!this.opts.address || !this.opts.port) {
- throw new Error('Address or port of running server were not defined; this ' +
- 'is required. This is probably a programming error in the driver');
- }
-
- if (!_.isNumber(timeout)) {
- throw new Error('Timeout parameter must be a number');
- }
-
- const driverOpts = {
- sessionId: this.sessionId,
- protocol: 'http', // Appium won't ever be behind ssl locally
- hostname: this.opts.address,
- port: this.opts.port,
- path: this.basePath,
- isW3C: this.isW3CProtocol(),
- isMobile: true,
- capabilities: this.caps
- };
- log.info(`Constructed webdriverio driver options; W3C mode is ${driverOpts.isW3C ? 'on' : 'off'}`);
-
-
- // fork the execution script as a child process
- const childScript = path.join(__dirname, 'execute-child.js');
- log.info(`Forking process to run webdriver script as child using ${childScript}`);
- const scriptProc = cp.fork(childScript);
-
- // keep track of whether we have canceled the script timeout, so we can stop
- // waiting for it and allow this process to finish gracefully
- let timeoutCanceled = false;
-
- try {
- const timeoutStart = Date.now();
-
- // promise that deals with the result from the child process
- const waitForResult = async function () {
- const resPromise = new B((res) => {
- scriptProc.on('message', res); // this is node IPC
- });
-
- const res = await resPromise;
- log.info('Received execute driver script result from child process, shutting it down');
-
- if (res.error) {
- throw new Error(res.error.message);
- }
-
- return res.success;
- };
-
- // promise that waits up to the timeout and throws an error if so, or does
- // nothing if the timeout is canceled because we got a result from the
- // child script
- const waitForTimeout = async function () {
- while (!timeoutCanceled && (Date.now() - timeoutStart) < timeout) {
- await B.delay(500);
- }
-
- if (timeoutCanceled) {
- return;
- }
-
- throw new Error(`Execute driver script timed out after ${timeout}ms. ` +
- `You can adjust this with the 'timeout' parameter.`);
- };
-
- // now that the child script is alive, send it the data it needs to start
- // running the driver script
- log.info('Sending driver and script data to child');
- scriptProc.send({driverOpts, script, timeout});
-
- // and set up a race between the response from the child and the timeout
- return await B.race([waitForResult(), waitForTimeout()]);
- } catch (err) {
- throw new Error(`Could not execute driver script. Original error was: ${err}`);
- } finally {
- // ensure we always cancel the timeout so that the timeout promise stops
- // spinning and allows this process to die gracefully
- timeoutCanceled = true;
-
- log.info('Disconnecting from and killing driver script child proc');
- scriptProc.disconnect();
- scriptProc.kill();
- }
-};
-
-
-export default commands;

MODIFY
packages/base-driver/lib/basedriver/commands/index.js
packages/base-driver/lib/basedriver/commands/index.js
@@ -3,7 +3,6 @@ import settingsCmds from './settings';
import timeoutCmds from './timeout';
import findCmds from './find';
import logCmds from './log';
-import executeCmds from './execute';
import eventCmds from './event';
@@ -16,7 +15,6 @@ Object.assign(
timeoutCmds,
findCmds,
logCmds,
- executeCmds,
eventCmds,
// add other command types here
);

MODIFY
packages/base-driver/lib/basedriver/driver.js
packages/base-driver/lib/basedriver/driver.js
@@ -342,7 +342,7 @@ class BaseDriver extends Protocol {
unexpectedShutdownListener = null;
}
});
- const res = this.isCommandsQueueEnabled && cmd !== 'executeDriverScript'
+ const res = this.isCommandsQueueEnabled
? await this.commandsQueueGuard.acquire(BaseDriver.name, commandExecutor)
: await commandExecutor();

MODIFY
packages/base-driver/lib/protocol/routes.js
packages/base-driver/lib/protocol/routes.js
@@ -575,9 +575,6 @@ const METHOD_MAP = {
'/session/:sessionId/appium/receive_async_response': {
POST: {command: 'receiveAsyncResponse', payloadParams: {required: ['response']}}
},
- '/session/:sessionId/appium/execute_driver': {
- POST: {command: 'executeDriverScript', payloadParams: {required: ['script'], optional: ['type', 'timeout']}}
- },
'/session/:sessionId/appium/events': {
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}}
},

MODIFY
packages/base-driver/package.json
packages/base-driver/package.json
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"validate.js": "0.13.1",
- "webdriverio": "6.12.1",
"ws": "7.5.5"
},
"publishConfig": {

MODIFY
packages/base-driver/test/basedriver/driver-e2e-tests.js
packages/base-driver/test/basedriver/driver-e2e-tests.js
import _ from 'lodash';
-import { BaseDriver, server, routeConfiguringFunction, DeviceSettings, errors } from '../../lib';
-import {
- MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY
-} from '../../lib/constants';
+import { BaseDriver, server, routeConfiguringFunction, DeviceSettings } from '../../lib';
import axios from 'axios';
import B from 'bluebird';
import {TEST_HOST, getTestPort, createAppiumURL, METHODS} from '../helpers';
@@ -339,181 +336,6 @@ function baseDriverE2ETests (DriverClass, defaultCaps = {}) {
});
});
- describe('execute driver script', function () {
- // mock some methods on BaseDriver that aren't normally there except in
- // a fully blown driver
- let originalFindElement, sessionId;
- before(function () {
- d.allowInsecure = ['execute_driver_script'];
- originalFindElement = d.findElement;
- d.findElement = (function (strategy, selector) {
- if (strategy === 'accessibility id' && selector === 'amazing') {
- return {[W3C_ELEMENT_KEY]: 'element-id-1'};
- }
-
- throw new errors.NoSuchElementError('not found');
- }).bind(d);
- });
-
- beforeEach(async function () {
- ({sessionId} = await startSession(defaultCaps));
- });
-
- after(function () {
- d.findElement = originalFindElement;
- });
-
- afterEach(async function () {
- await endSession(sessionId);
- });
-
- it('should not work unless the allowInsecure feature flag is set', async function () {
- d._allowInsecure = d.allowInsecure;
- try {
- d.allowInsecure = [];
- const script = `return 'foo'`;
- await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script, type: 'wd'},
- }).should.eventually.be.rejected;
- await endSession(sessionId);
- } finally {
- d.allowInsecure = d._allowInsecure;
- }
- });
-
- it('should execute a webdriverio script in the context of session', async function () {
- const script = `
- const timeouts = await driver.getTimeouts();
- const status = await driver.status();
- return [timeouts, status];
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script, type: 'webdriverio'},
- })).data;
- const expectedTimeouts = {command: 250, implicit: 0};
- const expectedStatus = {};
- value.result.should.eql([expectedTimeouts, expectedStatus]);
- });
-
- it('should fail with any script type other than webdriverio currently', async function () {
- const script = `return 'foo'`;
- await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script, type: 'wd'},
- }).should.eventually.be.rejected;
- });
-
- it('should execute a webdriverio script that returns elements correctly', async function () {
- const script = `
- return await driver.$("~amazing");
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script},
- })).data;
- value.result.should.eql({
- [W3C_ELEMENT_KEY]: 'element-id-1',
- [MJSONWP_ELEMENT_KEY]: 'element-id-1'
- });
- });
-
- it('should execute a webdriverio script that returns elements in deep structure', async function () {
- const script = `
- const el = await driver.$("~amazing");
- return {element: el, elements: [el, el]};
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script},
- })).data;
- const elObj = {
- [W3C_ELEMENT_KEY]: 'element-id-1',
- [MJSONWP_ELEMENT_KEY]: 'element-id-1'
- };
- value.result.should.eql({element: elObj, elements: [elObj, elObj]});
- });
-
- it('should store and return logs to the user', async function () {
- const script = `
- console.log("foo");
- console.log("foo2");
- console.warn("bar");
- console.error("baz");
- return null;
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script},
- })).data;
- value.logs.should.eql({log: ['foo', 'foo2'], warn: ['bar'], error: ['baz']});
- });
-
- it('should have appium specific commands available', async function () {
- const script = `
- return typeof driver.lock;
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- data: {script},
- })).data;
- value.result.should.eql('function');
- });
-
- it('should correctly handle errors that happen in a webdriverio script', async function () {
- const script = `
- return await driver.$("~notfound");
- `;
- const {data} = await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- validateStatus: null,
- data: {script},
- });
- data.value.result.error.error.should.equal('no such element');
- data.value.result.error.message.should.equal('not found');
- data.value.result.error.stacktrace.should.includes('NoSuchElementError:');
- data.value.result.selector.should.equal('~notfound');
- data.value.result.sessionId.should.equal(sessionId);
- });
-
- it('should correctly handle errors that happen when a script cannot be compiled', async function () {
- const script = `
- return {;
- `;
- const {data} = await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- validateStatus: null,
- data: {script},
- });
- data.value.should.have.property('message');
- data.value.message.should.match(/An unknown server-side error occurred while processing the command. Original error: Could not execute driver script. Original error was: Error: Unexpected token '?;'?/);
- });
-
- it('should be able to set a timeout on a driver script', async function () {
- const script = `
- await Promise.delay(1000);
- return true;
- `;
- const {value} = (await axios({
- url: createAppiumTestURL(sessionId, 'appium/execute_driver'),
- method: POST,
- validateStatus: null,
- data: {script, timeout: 50},
- })).data;
- value.message.should.match(/.+50.+timeout.+/);
- });
- });
-
if (DriverClass === BaseDriver) {
// only run this test on basedriver, not other drivers which also use these tests, since we
// don't want them to try and start sessions with these random capabilities that are

MODIFY
packages/base-driver/test/protocol/routes-specs.js
packages/base-driver/test/protocol/routes-specs.js
@@ -38,7 +38,7 @@ describe('Protocol', function () {
}
let hash = shasum.digest('hex').substring(0, 8);
// Modify the hash whenever the protocol has intentionally been modified.
- hash.should.equal('4d201f22');
+ hash.should.equal('ccbdbf87');
});
});

chore!(base-driver): remove support for execute driver script since it will be a plugin moving forward