432 lines
12 KiB
JavaScript
Executable File
432 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawn, exec } = require("child_process");
|
|
const commandLineArgs = require("command-line-args");
|
|
const ngrok = require("ngrok");
|
|
|
|
/************************************************** Global **************************************************/
|
|
|
|
// Get the command line options
|
|
const scriptName = path.basename(process.argv[1]);
|
|
const actionVerb = process.argv[2];
|
|
const options =
|
|
(actionVerb && actionVerb !== "--help") || !actionVerb === "-h"
|
|
? getOptions()
|
|
: {};
|
|
function getOptions() {
|
|
const optionDefinitions = [
|
|
{ name: "config", alias: "C", type: String },
|
|
{ name: "auth", alias: "a", type: String },
|
|
{ name: "authtoken", alias: "t", type: String },
|
|
{ name: "proto", alias: "P", type: String },
|
|
{ name: "port", alias: "p", type: Number },
|
|
{ name: "env", alias: "e", type: String },
|
|
// { name: "hostname", alias: "H", type: String },
|
|
{ name: "subdomain", alias: "s", type: String },
|
|
// { name: "config", alias: "c", type: String, multiple: true },
|
|
{ name: "region", alias: "r", type: String, defaultValue: "us" },
|
|
{ name: "verbose", alias: "v", type: Boolean, defaultValue: false },
|
|
{ name: "help", alias: "h", type: Boolean },
|
|
];
|
|
|
|
try {
|
|
// Parse options, skipping the first three arguments ('node', script name and verb)
|
|
const options = commandLineArgs(optionDefinitions, {
|
|
argv: process.argv.slice(3),
|
|
});
|
|
|
|
return options;
|
|
} catch (err) {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
if (options.verbose) console.log("Options:", options);
|
|
|
|
// Find the .env file
|
|
const dotEnvFile = options?.env ?? findDotEnv(".env");
|
|
if (!dotEnvFile) {
|
|
console.error(
|
|
`The specified environment file was not found: ${options?.env ?? ".env"}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (options.verbose) console.log(`Using environment file: ${dotEnvFile}`);
|
|
// Walk the path to find .env file...
|
|
function findDotEnv(filename) {
|
|
const currentDir = findProjectFolder(filename);
|
|
return `${currentDir}/${filename}`;
|
|
}
|
|
|
|
// Check if the ngrok config file exists
|
|
const ngrokConfigFile = options?.config ?? getNgrokConfig();
|
|
if (!ngrokConfigFile) {
|
|
console.error(
|
|
"No ngrok config file found! Run 'ngrok authtoken <token>' in your terminal."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (options.verbose) console.log(`Using ngrok config file: ${ngrokConfigFile}`);
|
|
function getNgrokConfig(pathOnly = false) {
|
|
const paths = [
|
|
os.homedir() + "/.ngrok2", // Newer versions of ngrok
|
|
os.homedir() + "/Library/Application Support/ngrok", // MacOS
|
|
os.homedir() + "/AppData/Local/ngrok", // Windows
|
|
os.homedir() + "/.config/ngrok", // Alternative location for linux
|
|
];
|
|
for (const p of paths) {
|
|
if (fs.existsSync(`${p}/ngrok.yml`)) {
|
|
return pathOnly ? p : `${p}/ngrok.yml`;
|
|
}
|
|
}
|
|
|
|
if (pathOnly) return null;
|
|
throw new Error(
|
|
"Ngrok config file not found. Try running 'ngrok authtoken <token>' in your terminal."
|
|
);
|
|
}
|
|
|
|
// Read the ngrok config file
|
|
const authToken = options?.authtoken ?? readNgrokConfig(ngrokConfigFile);
|
|
if (!authToken) {
|
|
console.error(
|
|
"No ngrok authtoken found! Run 'ngrok authtoken <token>' in your terminal."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
function readNgrokConfig() {
|
|
const data = fs.readFileSync(ngrokConfigFile, "utf8");
|
|
|
|
let authToken;
|
|
|
|
// Convert string to string array, split at newlines
|
|
let lines = data.split("\n"); // string[]
|
|
|
|
// find the auth token (format: authtoken: token_goes_here)
|
|
lines.forEach((element) => {
|
|
const [name, value] = element.split(": ");
|
|
if (name === "authtoken") authToken = value;
|
|
});
|
|
|
|
// No token found
|
|
if (!authToken) {
|
|
// https://dashboard.ngrok.com/get-started/your-authtoken
|
|
return null;
|
|
}
|
|
|
|
return authToken;
|
|
}
|
|
|
|
// Setup the ngenv folder
|
|
const tmpFolder = os.tmpdir();
|
|
if (!tmpFolder) {
|
|
console.error(
|
|
"No project folder found! Make sure there is a .env file in the project folder."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (options.verbose) console.log(`Using project folder: ${tmpFolder}`);
|
|
if (!fs.existsSync(`${tmpFolder}/.ngenv`)) {
|
|
fs.mkdirSync(`${tmpFolder}/.ngenv`);
|
|
}
|
|
const lockFile = `${tmpFolder}/.ngenv/running.lock`;
|
|
const outLogFile = `${tmpFolder}/.ngenv/out.log`;
|
|
const errLogFile = `${tmpFolder}/.ngenv/err.log`;
|
|
|
|
/******************************************* Startup *******************************************/
|
|
|
|
// Show the root help
|
|
if (!actionVerb || actionVerb === "--help" || actionVerb === "-h") {
|
|
rootHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
// Show the command specific help
|
|
if (options.help) {
|
|
commandHelp(actionVerb);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Process command line arguments
|
|
switch (actionVerb) {
|
|
case "start":
|
|
// Spawn the background process
|
|
startBackgroundProcess();
|
|
break;
|
|
case "stop":
|
|
// Stop the background process
|
|
stopBackgroundProcess();
|
|
break;
|
|
case "clear":
|
|
// Clear the log files
|
|
clearLogs();
|
|
break;
|
|
case "logs":
|
|
// Display the log files
|
|
displayLogs();
|
|
break;
|
|
case "run":
|
|
case "background": {
|
|
// Set up the background process
|
|
if (actionVerb === "background") {
|
|
// Start-up for the background process
|
|
process.on("SIGTERM", () => {
|
|
console.log("Stopping background process...");
|
|
removeLockFile();
|
|
process.exit(0);
|
|
});
|
|
process.on("exit", (code) => {
|
|
console.log(`About to exit with code: ${code}`);
|
|
// Perform cleanup or final operations here
|
|
removeLockFile();
|
|
});
|
|
}
|
|
|
|
// Start the main process
|
|
main();
|
|
break;
|
|
}
|
|
default:
|
|
console.log(`Unknown command: ${actionVerb}`);
|
|
console.log(`Run '${scriptName} --help' for usage.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function rootHelp() {
|
|
console.log(`Usage: ${scriptName} [run|start|stop|logs|clear] [options]`);
|
|
console.log(`\nCommands:`);
|
|
console.log(` run\t\t\tRun the tunnel in the foreground.`);
|
|
console.log(` start\t\t\tStart the tunnel in the background.`);
|
|
console.log(` stop\t\t\tStop the background process if it's running.`);
|
|
console.log(` logs\t\t\tDisplay the output and error logs.`);
|
|
console.log(` clear\t\t\tClear the content of the output and error logs.`);
|
|
console.log(`\nOptions:`);
|
|
console.log(` -h, --help\t\tDisplay this help message.`);
|
|
console.log(`\nExample:`);
|
|
console.log(` ${scriptName} start\t\t# Starts the background process`);
|
|
}
|
|
|
|
function commandHelp(command) {
|
|
console.log(`Usage: ${scriptName} ${command} [options]`);
|
|
console.log(`\nOptions:`);
|
|
if (command === "start" || command === "run") {
|
|
console.log(
|
|
` -P, --proto\t\tProtocol to use (http|tcp|tls). Default is http.`
|
|
);
|
|
console.log(` -p, --port\t\tPort to use. Default is 3000.`);
|
|
console.log(` -s, --subdomain\tSubdomain to use; [xyz].ngrok.io`);
|
|
console.log(` -r, --region\t\tRegion to use. Default is 'us'.`);
|
|
console.log(` -a, --auth\t\tHTTP Basic authentication for tunnel.`);
|
|
console.log(
|
|
` -t, --authtoken\tYour authtoken from ngrok.com. Uses config file if not provided.`
|
|
);
|
|
console.log(` -e, --env\t\tEnvironment file to use. Default is .env.`);
|
|
console.log(` -C, --config\t\tCustom path to ngrok config file.`);
|
|
console.log(` -v, --verbose\t\tShow verbose output.`);
|
|
}
|
|
console.log(` -h, --help\t\tDisplay this help message.`);
|
|
}
|
|
|
|
/******************************************* Main *******************************************/
|
|
|
|
async function main() {
|
|
// Get the ngrok config file path
|
|
const ngrokConfig = options.config ?? getNgrokConfig();
|
|
|
|
// Check if the ngrok config file exists
|
|
if (!fs.existsSync(ngrokConfig)) {
|
|
throw new Error("No ngrok config file found");
|
|
}
|
|
|
|
// Start ngrok
|
|
const proto = options?.proto ?? "http";
|
|
const addr = options?.port ? options.port : 3000;
|
|
try {
|
|
const url = await ngrok.connect({
|
|
authtoken: authToken,
|
|
proto,
|
|
addr,
|
|
region: options?.region,
|
|
auth: options?.auth,
|
|
subdomain: options?.subdomain,
|
|
configPath: ngrokConfig,
|
|
onLogEvent: (data) => {
|
|
// console.log(data);
|
|
},
|
|
});
|
|
writeDotEnv(url); // Sync the .env file
|
|
console.log(`Started ngrok: protocol '${proto}', addr '${addr}'`);
|
|
console.log(`Your current ngrok url is ${url}.`);
|
|
console.log(`Your .env file has been updated.`);
|
|
} catch (err) {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/******************************************* Functions *******************************************/
|
|
|
|
function isAlreadyRunning() {
|
|
return fs.existsSync(lockFile);
|
|
}
|
|
|
|
function createLockFile(pid) {
|
|
fs.writeFileSync(lockFile, pid.toString());
|
|
}
|
|
|
|
function removeLockFile() {
|
|
if (isAlreadyRunning()) {
|
|
fs.unlinkSync(lockFile);
|
|
}
|
|
}
|
|
|
|
function removeLogFiles() {
|
|
if (fs.existsSync(outLogFile)) {
|
|
fs.unlinkSync(outLogFile);
|
|
}
|
|
if (fs.existsSync(errLogFile)) {
|
|
fs.unlinkSync(errLogFile);
|
|
}
|
|
}
|
|
|
|
function startBackgroundProcess() {
|
|
if (isAlreadyRunning()) {
|
|
console.log("An instance is already running.");
|
|
process.exit(1);
|
|
}
|
|
|
|
// Remove old log files
|
|
removeLogFiles();
|
|
|
|
const out = fs.openSync(outLogFile, "a");
|
|
const err = fs.openSync(errLogFile, "a");
|
|
|
|
const subprocess = spawn(
|
|
process.argv[0],
|
|
[process.argv[1], "background", ...process.argv.slice(3)],
|
|
{
|
|
detached: true,
|
|
stdio: ["ignore", out, err],
|
|
}
|
|
);
|
|
|
|
createLockFile(subprocess.pid);
|
|
console.log(`Started background process with PID: ${subprocess.pid}`);
|
|
|
|
subprocess.unref();
|
|
}
|
|
|
|
function stopBackgroundProcess() {
|
|
if (!isAlreadyRunning()) {
|
|
console.log("No running instance found.");
|
|
return;
|
|
}
|
|
|
|
const pid = fs.readFileSync(lockFile, "utf-8");
|
|
|
|
exec(`ps -p ${pid}`, (error, stdout, stderr) => {
|
|
if (error || stderr) {
|
|
console.log(`No process with PID ${pid} is running.`);
|
|
removeLockFile();
|
|
return;
|
|
}
|
|
|
|
// If the process exists, attempt to kill it
|
|
try {
|
|
process.kill(pid, "SIGTERM");
|
|
removeLockFile();
|
|
console.log(`Stopped process with PID: ${pid}`);
|
|
} catch (err) {
|
|
console.error(
|
|
`Failed to stop process with PID: ${pid}. Error: ${err.message}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function readLog(path) {
|
|
try {
|
|
return fs.readFileSync(path, "utf-8");
|
|
} catch (err) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function displayLogs() {
|
|
const outLog = readLog(outLogFile);
|
|
const errLog = readLog(errLogFile);
|
|
|
|
console.log("STDOUT:\n", outLog.trimStart());
|
|
console.log("STDERR:\n", errLog.trimStart());
|
|
}
|
|
|
|
function clearLogs() {
|
|
fs.rmSync(outLogFile, { force: true });
|
|
fs.rmSync(errLogFile, { force: true });
|
|
|
|
console.log("Log files cleared.");
|
|
}
|
|
|
|
function findProjectFolder(filename = ".env") {
|
|
// Walk the path to find .env file... options?.env
|
|
let currentDir = process.cwd();
|
|
while (true) {
|
|
const packageJsonPath = path.join(currentDir, "package.json");
|
|
if (fs.existsSync(packageJsonPath)) {
|
|
break;
|
|
}
|
|
const envPath = path.join(currentDir, filename);
|
|
if (fs.existsSync(envPath)) {
|
|
break;
|
|
}
|
|
const parentDir = path.resolve(currentDir, "..");
|
|
if (parentDir === currentDir) {
|
|
throw new Error("No project folder found");
|
|
}
|
|
currentDir = parentDir;
|
|
}
|
|
return currentDir;
|
|
}
|
|
|
|
function readDotEnv(path) {
|
|
const data = fs.readFileSync(path, "utf8");
|
|
|
|
// Convert string to string array, split at newlines
|
|
let lines = data.split("\n"); // string[]
|
|
|
|
return lines;
|
|
}
|
|
|
|
function writeDotEnv(url) {
|
|
// Read the .env file
|
|
let lines = readDotEnv(dotEnvFile);
|
|
|
|
// Rebuild lines with the new url
|
|
let found = false;
|
|
lines = lines.map((element) => {
|
|
const [name] = element.split("=");
|
|
if (name === "NGROK_SERVERHOST") {
|
|
found = true;
|
|
return `${name}=${url}`;
|
|
}
|
|
return element;
|
|
});
|
|
|
|
// Is this variable already in the .env, if not add it.
|
|
if (!found) {
|
|
lines.unshift(`NGROK_SERVERHOST=${url}`);
|
|
}
|
|
|
|
// convert back to string format
|
|
let writeData = "";
|
|
lines.forEach((element) => {
|
|
writeData += element + "\n";
|
|
});
|
|
writeData = writeData.slice(0, writeData.length - 1);
|
|
|
|
// Write the new .env file
|
|
fs.writeFileSync(dotEnvFile, writeData);
|
|
}
|