@wdio/appium-service
Version:
A WebdriverIO service to start & stop Appium Server
236 lines (234 loc) • 8.14 kB
JavaScript
// src/cli-utils.ts
import { resolve } from "node:path";
import url from "node:url";
import { resolve as resolveModule } from "import-meta-resolve";
import { execSync, spawn, exec } from "node:child_process";
import os from "node:os";
import { promisify } from "node:util";
var APPIUM_START_TIMEOUT = 30 * 1e3;
function extractPortFromArgs(args) {
const portArgIndex = args.findIndex((arg) => arg.startsWith("--port"));
if (portArgIndex === -1) {
return null;
}
const portArg = args[portArgIndex];
let portValue;
if (portArg.includes("=")) {
portValue = portArg.split("=")[1];
} else {
const nextArg = args[portArgIndex + 1];
if (!nextArg || nextArg.startsWith("--")) {
throw new Error("Missing port value after --port flag.");
}
portValue = nextArg;
}
const port = Number(portValue);
if (Number.isInteger(port) && port >= 1 && port <= 65535) {
return port;
}
return null;
}
function extractPortFromCliArgs(args) {
return extractPortFromArgs(args) ?? 4723;
}
function removePortFromArgs(args) {
for (let i = args.length - 1; i >= 0; i--) {
if (args[i].startsWith("--port")) {
const portArg = args[i];
if (portArg.includes("=")) {
args.splice(i, 1);
} else {
args.splice(i, 2);
}
}
}
}
async function tryResolveModule(command, from) {
try {
const entryPath = await resolveModule(command, from);
return url.fileURLToPath(entryPath);
} catch {
return null;
}
}
async function determineAppiumCliCommand(command = "appium") {
const localNodeModules = resolve(process.cwd(), "node_modules");
const localPath = await tryResolveModule(command, url.pathToFileURL(localNodeModules).toString());
if (localPath) {
return localPath;
}
const packagePath = await tryResolveModule(command, import.meta.url);
if (packagePath) {
return packagePath;
}
try {
const npmPrefix = execSync("npm config get prefix", { encoding: "utf-8" }).trim();
const globalNodeModules = resolve(npmPrefix, "lib", "node_modules");
const globalPath = await tryResolveModule(command, url.pathToFileURL(globalNodeModules).toString());
if (globalPath) {
return globalPath;
}
} catch {
}
throw new Error(
"Appium is not installed. Please install it globally via `npm install -g appium`\nor locally in your project via `npm i --save-dev appium`. Do not forget to also\ninstall the drivers for your platform."
);
}
async function checkInspectorPluginInstalled(appiumCommandPath) {
const INSPECTOR_PLUGIN_DOCS_URL = "https://appium.github.io/appium-inspector/latest/quickstart/installation/#appium-plugin";
const helpMessage = `Please check this link for more information: ${INSPECTOR_PLUGIN_DOCS_URL}`;
try {
const { stdout, stderr } = await promisify(exec)(`${appiumCommandPath} plugin list --installed`, {
encoding: "utf-8"
});
const output = stderr || stdout;
if (!output || output.trim().length === 0) {
throw new Error(
`Appium Inspector plugin is not installed. ${helpMessage}`
);
}
const lines = output.split("\n");
const inspectorLine = lines.find((line) => /^-.*inspector/i.test(line.trim()));
if (!inspectorLine) {
throw new Error(
`Appium Inspector plugin is not installed. ${helpMessage}`
);
}
} catch (err) {
if (err instanceof Error && !err.message.includes("Inspector plugin")) {
throw new Error(
`Failed to check Appium Inspector plugin installation: ${err.message}. ${helpMessage}`
);
}
throw err;
}
}
async function startAppiumForCli(appiumCommandPath, args, timeout = APPIUM_START_TIMEOUT) {
let command = "node";
const appiumArgs = [appiumCommandPath, ...args];
if (os.platform() === "win32") {
appiumArgs.unshift("/c", command);
command = "cmd";
}
const appiumProcess = spawn(command, appiumArgs, {
stdio: ["ignore", "pipe", "pipe"]
});
let errorCaptured = false;
let timeoutId;
let error;
return new Promise((resolvePromise, reject) => {
let outputBuffer = "";
timeoutId = setTimeout(() => {
rejectOnce(new Error("Timeout: Appium did not start within expected time"));
}, timeout);
const rejectOnce = (err) => {
if (!errorCaptured) {
errorCaptured = true;
clearTimeout(timeoutId);
reject(err);
}
};
const onErrorMessage = (data) => {
const message = data.toString();
const isDebuggerMessage = message.includes("Debugger attached") || message.includes("Debugger listening on") || message.includes("For help, see: https://nodejs.org/en/docs/inspector");
if (isDebuggerMessage) {
return;
}
error = (error || "") + message;
process.stderr.write(message);
};
const onStdout = (data) => {
outputBuffer += data.toString();
process.stdout.write(data.toString());
if (outputBuffer.includes("Appium REST http interface listener started")) {
outputBuffer = "";
clearTimeout(timeoutId);
resolvePromise(appiumProcess);
}
};
appiumProcess.stdout.on("data", onStdout);
appiumProcess.stderr.on("data", onErrorMessage);
appiumProcess.once("exit", (exitCode) => {
let errorMessage = `Appium exited before timeout (exit code: ${exitCode})`;
if (exitCode === 2) {
errorMessage += "\n" + (error || "Check that you don't already have a running Appium service.");
} else if (error) {
errorMessage += `
${error}`;
}
if (exitCode !== 0) {
console.error(errorMessage);
}
rejectOnce(new Error(errorMessage));
});
});
}
async function openBrowser(url2) {
console.log("\u{1F310} Opening Appium Inspector in your default browser...");
let command;
const platform = os.platform();
if (platform === "win32") {
command = `start "" "${url2}"`;
} else if (platform === "darwin") {
command = `open "${url2}"`;
} else {
command = `xdg-open "${url2}"`;
}
try {
execSync(command, { stdio: "ignore" });
console.log("\u2705 Opened Appium Inspector in your default browser.");
} catch {
console.warn(`\u26A0\uFE0F Automatically starting the default browser didn't work, please open your favorite browser and paste the url '${url2}' in there`);
}
}
// src/cli.ts
import treeKill from "tree-kill";
import { promisify as promisify2 } from "node:util";
var promisifiedTreeKill = promisify2(treeKill);
async function run() {
const args = process.argv.slice(2);
const port = extractPortFromCliArgs(args);
const requiredFlags = ["--log-timestamp", "--use-plugins=inspector", "--allow-cors"];
removePortFromArgs(args);
args.unshift(`--port=${port}`);
requiredFlags.forEach((flag) => {
if (!args.includes(flag)) {
args.push(flag);
}
});
const command = await determineAppiumCliCommand();
const serverArgs = ["server", ...args];
console.log("\u23F3 Checking inspector plugin...");
await checkInspectorPluginInstalled(command);
console.log("\u{1F680} Starting Appium server...");
console.log(`\u{1F4E1} Command: ${command} ${serverArgs.join(" ")}`);
console.log("\u23F3 Waiting for Appium server to be ready...");
console.log("\u2139\uFE0F Press Ctrl+C to stop Appium server and exit\n\n");
const appiumProcess = await startAppiumForCli(command, serverArgs);
await openBrowser(`http://localhost:${port}/inspector`);
let isCleaningUp = false;
const cleanup = async () => {
if (isCleaningUp) {
return;
}
isCleaningUp = true;
if (appiumProcess && appiumProcess.pid) {
console.log("\n\u{1F6D1} Stopping Appium server...");
try {
await promisifiedTreeKill(appiumProcess.pid, "SIGTERM");
console.log("\u2705 Appium server stopped successfully");
} catch (err) {
console.error("Error stopping Appium:", err);
}
}
process.removeListener("SIGINT", cleanup);
process.removeListener("SIGTERM", cleanup);
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
process.stdin.resume();
}
export {
run
};