Skip to content

Commit

Permalink
feat: Move WAD server installation into a driver script (#277)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: WAD is does not get installed automatically anymore upon driver install

From now on the driver install won't include the automatic server deployment. Instead it is expected that WAD server is installed via the driver script: `appium driver run windows install-wad <optional_wad_version>`.
  • Loading branch information
mykola-mokhnach authored Sep 16, 2024
1 parent 609b501 commit 23ab085
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 172 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Beside of standard Appium requirements Appium Windows Driver adds the following

- Appium Windows Driver only supports Windows 10 as the host.
- [Developer mode](https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development) must be enabled
- Appium downloads and installs WinAppDriver package automatically upon executing its installation scripts, although, the actual binary version could be out of date. In such case you could download and install the most recent version of WinAppDriver manually from the [GitHub releases](https://github.com/microsoft/WinAppDriver/releases) page.
- Since version 3.0.0 this driver **does not** automatically install WinAppDriver server anymore. You should perform the server installation via the [install-wad](#install-wad) driver script instead. Driver versions below 3.0.0 download and install a bundled WinAppDriver package version automatically upon executing its installation via the Appium command line interface. Although, in such case the actual server binary version could be out of date. You could download and install the most recent version of WinAppDriver server manually from the [GitHub releases](https://github.com/microsoft/WinAppDriver/releases) page.

Appium Windows Driver supports the following capabilities:

Expand All @@ -44,6 +44,15 @@ appium:prerun | An object containing either `script` or `command` key. The value
appium:postrun | An object containing either `script` or `command` key. The value of each key must be a valid PowerShell script or command to be executed after WinAppDriver session is stopped. See [Power Shell commands execution](#power-shell-commands-execution) for more details.
appium:newCommandTimeout | How long (in seconds) the driver should wait for a new command from the client before assuming the client has stopped sending requests. After the timeout, the session is going to be deleted. `60` seconds by default. Setting it to zero disables the timer.

## Driver Scripts

### install-wad

This script is used to install the given or latest stable version of WinAppDriver server from
the [GitHub releases](https://github.com/microsoft/WinAppDriver/releases) page.
Run `appium driver run windows install-wad <optional_wad_version>`, where `optional_wad_version`
must be either valid WAD version number or should not be present (the latest stable version is used then).

## Example

```python
Expand Down
32 changes: 0 additions & 32 deletions install-npm.js

This file was deleted.

80 changes: 9 additions & 71 deletions lib/installer.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import _ from 'lodash';
import { system, fs, util, net, tempDir } from 'appium/support';
import { fs, tempDir } from 'appium/support';
import path from 'path';
import { exec } from 'teen_process';
import log from './logger';
import { queryRegistry } from './registry';
import { shellExec } from './utils';

// https://github.com/microsoft/WinAppDriver/releases
const WAD_VER = '1.2.99';
const WAD_DOWNLOAD_MD5 = Object.freeze({
x32: '23745e6ed373bc969ff7c4493e32756a',
x64: '2923fc539f389d47754a7521ee50108e',
arm64: 'b9af4222a3fb0d688ecfbf605d1c4500',
});
const ARCH_MAPPING = Object.freeze({x32: 'x86', x64: 'x64', arm64: 'arm64'});
const WAD_DOWNLOAD_TIMEOUT_MS = 60000;
const POSSIBLE_WAD_INSTALL_ROOTS = [
process.env['ProgramFiles(x86)'],
process.env.ProgramFiles,
Expand All @@ -33,17 +24,6 @@ session.DoAction("CostFinalize")
WScript.Echo session.Property("INSTALLFOLDER")
`.replace(/\n/g, '\r\n');


function generateWadDownloadLink () {
const wadArch = ARCH_MAPPING[process.arch];
if (!wadArch) {
throw new Error(`System architecture '${process.arch}' is not supported by Windows Application Driver. ` +
`The only supported architectures are: ${_.keys(ARCH_MAPPING)}`);
}
return `https://github.com/Microsoft/WinAppDriver` +
`/releases/download/v${WAD_VER}/WindowsApplicationDriver-${WAD_VER}-win-${wadArch}.exe`;
}

async function fetchMsiInstallLocation (installerGuid) {
const tmpRoot = await tempDir.openDir();
const scriptPath = path.join(tmpRoot, 'get_wad_inst_location.vbs');
Expand All @@ -58,7 +38,7 @@ async function fetchMsiInstallLocation (installerGuid) {

class WADNotFoundError extends Error {}

const getWADExecutablePath = _.memoize(async function getWADInstallPath () {
export const getWADExecutablePath = _.memoize(async function getWADInstallPath () {
const wadPath = process.env.APPIUM_WAD_PATH ?? '';
if (await fs.exists(wadPath)) {
log.debug(`Loaded WinAppDriver path from the APPIUM_WAD_PATH environment variable: ${wadPath}`);
Expand Down Expand Up @@ -105,61 +85,19 @@ const getWADExecutablePath = _.memoize(async function getWADInstallPath () {
log.debug(e.stack);
}
throw new WADNotFoundError(`${WAD_EXE_NAME} has not been found in any of these ` +
`locations: ${pathCandidates}. Is it installed?`);
`locations: ${pathCandidates}. Use the following driver script to install it: ` +
`'appium driver run windows install-wad <optional_wad_version>'. ` +
`Check https://github.com/microsoft/WinAppDriver/releases to list ` +
`available server versions or drop the '<optional_wad_version>' argument to ` +
`install the latest stable one.`
);
});

async function downloadWAD () {
const downloadLink = generateWadDownloadLink();
const installerPath = path.resolve(await tempDir.staticDir(),
`wad_installer_${WAD_VER}_${util.uuidV4()}.exe`);
log.info(`Downloading ${downloadLink} to '${installerPath}'`);
await net.downloadFile(downloadLink, installerPath, {timeout: WAD_DOWNLOAD_TIMEOUT_MS});
const downloadedMd5 = await fs.md5(installerPath);
const expectedMd5 = WAD_DOWNLOAD_MD5[process.arch];
if (downloadedMd5 !== expectedMd5) {
await fs.rimraf(installerPath);
throw new Error(
`Installer executable checksum validation error: expected ${expectedMd5} but got ${downloadedMd5}`
);
}
return installerPath;
}

const isAdmin = async function isAdmin () {
export async function isAdmin () {
try {
await exec('fsutil.exe', ['dirty', 'query', process.env.SystemDrive || 'C:']);
return true;
} catch (ign) {
return false;
}
};

async function setupWAD () {
if (!system.isWindows()) {
throw new Error(`Can only download WinAppDriver on Windows!`);
}

try {
return await getWADExecutablePath();
} catch (e) {
if (!(e instanceof WADNotFoundError)) {
throw e;
}
log.info(`WinAppDriver doesn't exist, setting up`);
}

const installerPath = await downloadWAD();
log.info(`Running WinAppDriver v${WAD_VER} installer`);
try {
await shellExec(installerPath, ['/install', '/quiet', '/norestart']);
} catch (e) {
/** @type {import('node:child_process').ExecException} */
const error = e;
throw new Error(`WinAppDriver cannot be installed: ${error.message}. Exit code: ${error.code}`);
} finally {
await fs.rimraf(installerPath);
}
}

export { downloadWAD, setupWAD, getWADExecutablePath, isAdmin };
export default setupWAD;
3 changes: 1 addition & 2 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { logger } from 'appium/support';


let log = logger.getLogger('WinAppDriver');
export const log = logger.getLogger('WinAppDriver');

export default log;
12 changes: 11 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { util} from 'appium/support';
import { util, net } from 'appium/support';
import { promisify } from 'node:util';
import { exec } from 'node:child_process';
import B from 'bluebird';
Expand Down Expand Up @@ -43,3 +43,13 @@ export function requireArgs (argNames, opts) {
}
return opts;
}

/**
*
* @param {string} srcUrl
* @param {string} dstPath
* @returns {Promise<void>}
*/
export async function downloadToFile(srcUrl, dstPath) {
await net.downloadFile(srcUrl, dstPath);
}
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,29 @@
"platformNames": [
"Windows"
],
"scripts": {
"install-wad": "./scripts/install-wad.mjs"
},
"mainClass": "WindowsDriver"
},
"files": [
"index.js",
"install-npm.js",
"lib",
"build/index.js",
"build/install-npm.js",
"build/lib",
"CHANGELOG.md",
"LICENSE",
"npm-shrinkwrap.json"
"npm-shrinkwrap.json",
"scripts"
],
"dependencies": {
"asyncbox": "^3.0.0",
"axios": "^1.7.7",
"bluebird": "^3.5.1",
"lodash": "^4.6.1",
"portscanner": "^2.2.0",
"semver": "^7.6.3",
"source-map-support": "^0.x",
"teen_process": "^2.0.1"
},
Expand All @@ -64,7 +69,6 @@
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix",
"prepare": "npm run build",
"install": "node install-npm.js",
"test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"",
"e2e-test": "mocha --exit --timeout 10m \"./test/e2e/**/*-specs.js\""
},
Expand Down
Loading

0 comments on commit 23ab085

Please sign in to comment.