UNPKG

@wdio/appium-service

Version:
236 lines (234 loc) 8.14 kB
// 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 };