#!/usr/bin/env node /////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2019 AcceleratXR, Inc. All rights reserved. /////////////////////////////////////////////////////////////////////////////// import config from "./config"; import { Server } from "@acceleratxr/services_common"; import { Logger } from "@acceleratxr/service_utilities"; import JobCoordinator from "./JobCoordinator"; import * as fs from "fs"; import * as path from "path"; import * as process from "process"; import { Job } from "./Job"; import CLIVisualizer from "./CLIVisualizer"; const commandLineArgs = require("command-line-args"); const packageInfo = require("../package.json"); const printHelp = function() { console.log(`Usage: ${packageInfo.name} [OPTIONS]`); console.log(""); console.log("A clustered and easily scriptable load testing tool."); console.log(""); console.log("OPTIONS:"); console.log("\t\t-i --input\tThe test script to run."); console.log("\t\t-d --duration\tThe length of time to perform the test (e.g. 10s, 1m, 1h)"); console.log("\t\t-u --vus\tThe number of virtual users to simulate."); console.log("\t\t-m --master\tThe host address of the master coordinator."); console.log("\t\t\t\tSet this to have the local instance run as a worker node in a cluster."); console.log("\t\t-s --headless\tRuns the tool in headless mode with CLI output only."); console.log("\t\t\t\tNote that an input file and vus must be specified to run in headless mode."); console.log("\t\t-q --quit\tDisables the progress bar."); console.log("\t\t-v --version\tPrints the version of the tool."); console.log("\t\t-h --help\tDisplays this help dialog."); console.log(""); console.log("EXAMPLES:"); console.log(""); console.log("\t\ttsunami"); console.log("\t\t\tStarts a local master coordinator with a web UI."); console.log(""); console.log("\t\ttsunami -s -i mytest.ts -u 1000"); console.log("\t\t\tRuns the test file mytest.ts in CLI mode with 1000 simulated users."); console.log(""); console.log("\t\ttsunami -s -i mytest.ts -u 1000 -d 10m"); console.log( "\t\t\tRuns the test file mytest.ts in CLI mode with 1000 simulated users for a continuous duration of 10m." ); console.log(""); console.log(`\t\ttsunami -m http://master:${config.get("port")}`); console.log( "\t\t\tStarts the tool as a remote job runner and connects to the master coordinator at the specified address." ); console.log(""); console.log("Copyright (C) 2019 AcceleratXR, Inc. All rights reserved."); console.log(""); console.log("Documentation can be found at https://tsunamijs.io"); }; const printVersion = function() { console.log(`v${packageInfo.version}`); }; // Parse the CLI options const cliOptions = commandLineArgs([ { name: "input", alias: "i", type: String }, { name: "duration", alias: "d", type: String }, { name: "vus", alias: "u", type: Number }, { name: "master", alias: "m", type: String }, { name: "headless", alias: "s", type: Boolean }, { name: "quiet", alias: "q", type: Boolean }, { name: "version", alias: "v", type: Boolean }, { name: "help", alias: "h", type: Boolean }, ], { stopAtFirstUnknown: true }); const logger = Logger(cliOptions["verbose"] ? "debug" : "info"); let server: Server | undefined = undefined; const printTestResults = function(): void { // Check to see if the coordinator has completed all queued jobs let successes: number = 0; let failures: number = 0; let tests: number = 0; // We just need to find one job still in the running state for (const status of JobCoordinator.jobStatus.values()) { successes += status.successes; failures += status.failures; tests += 1; } logger.info("Test Results:"); logger.info("Tests: " + tests); logger.info("Successes: " + successes); logger.info("Failures: " + failures); }; const shutdown = async function(): Promise { logger.info("Shutting down..."); JobCoordinator.shutdown(); if (server) { await server.stop(); } CLIVisualizer.stop(); if (cliOptions["headless"]) { printTestResults(); } process.exit(0); }; const processCLI = async () => { if (cliOptions["help"]) { printHelp(); process.exit(0); return; } if (cliOptions["version"]) { printVersion(); process.exit(0); return; } // Is this a node worker or a master? if (cliOptions["master"]) { // TODO Start as remote job runner } else { await JobCoordinator.init(logger, true); if (cliOptions["headless"]) { // Wait a moment before proceeding to allow the local thread pool to fully start up setTimeout(() => { // Make sure all required options have been set. if (!cliOptions["input"] || (!cliOptions["duration"] && !cliOptions["vus"])) { printHelp(); process.exit(1); return; } // Read in the contents of the input file and queue a new job with the coordinator let file = path.resolve(cliOptions["input"]); if (fs.existsSync(file)) { const script: string = fs.readFileSync(file, "utf-8"); const job: Job = new Job({ name: path.basename(file), script, duration: cliOptions["duration"], vus: cliOptions["vus"], args: cliOptions["_unknown"] }); JobCoordinator.queue(job); } else { logger.error("No such file exists: " + file); process.exit(1); return; } // Set up CLI visualizer if (!cliOptions["quiet"]) { CLIVisualizer.init(logger); CLIVisualizer.start(); } // Set up end of job completion listener const interval: NodeJS.Timeout = setInterval(() => { // Check to see if the coordinator has completed all queued jobs if (JobCoordinator.jobs.size === 0) { // We just need to find one job still in the running state let running: boolean = false; for (const status of JobCoordinator.jobStatus.values()) { if (status.state === "RUNNING") { running = true; break; } } // If no more jobs are running then we've finished the test. if (!running) { clearInterval(interval); shutdown(); } } }, 1000); }, 2000); } else { // Create and start the AcceleratXRâ„¢ server server = new Server(config, undefined, "./dist", logger); await server.start(); logger.info(""); logger.info(`Open a browser to http://localhost:${config.get("port")}/app`); // TODO Pre-load the web UI with the input scripts } } }; process.on("SIGINT", () => { shutdown(); }); processCLI();