ngenv/ngenv

344 lines
8.8 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");
const projectFolder = findProjectFolder();
if (!projectFolder) {
console.error("No project folder found");
process.exit(1);
}
if (!fs.existsSync(`${projectFolder}/.ngenv`)) {
fs.mkdirSync(`${projectFolder}/.ngenv`);
}
const lockFile = `${projectFolder}/.ngenv/running.lock`;
const outLogFile = `${projectFolder}/.ngenv/out.log`;
const errLogFile = `${projectFolder}/.ngenv/err.log`;
let authToken = null; // string || null
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 getOptions() {
const optionDefinitions = [
{ name: "proto", alias: "P", type: String },
{ name: "port", alias: "p", type: Number },
{ name: "env", alias: "e", type: String },
];
// Parse options, skipping the first three arguments ('node', script name and verb)
const options = commandLineArgs(optionDefinitions, {
argv: process.argv.slice(3),
});
return options;
}
// Process command line arguments
if (process.argv[2] === "start") {
// The result is only used in the background process, but the check is also done here, before the
// background process is started to prevent bad arguments from being passed to the background process.
getOptions();
// Spawn the background process
startBackgroundProcess();
} else if (process.argv[2] === "stop") {
// Stop the background process
stopBackgroundProcess();
} else if (process.argv[2] === "clear") {
// Clear the log files
clearLogs();
} else if (process.argv[2] === "logs") {
// Display the log files
displayLogs();
// tailLogs();
} else if (process.argv[2] === "run" || process.argv[2] === "background") {
if (process.argv[2] === "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();
});
}
const options = getOptions();
backgroundMain(options);
} else {
const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [run|start|stop|logs|clear]`);
console.log(`\nCommands:`);
console.log(
` start\t\tStart the background process if it's not already running.`
);
console.log(` stop\t\tStop the background process if it's running.`);
console.log(` logs\t\tDisplay the output and error logs.`);
console.log(` clear\t\tClear the content of the output and error logs.`);
console.log(`\nExample:`);
console.log(` node ${scriptName} start\t\t# Starts the background process`);
}
async function backgroundMain(options) {
// Get the ngrok config file path
let ngrokConfig = getNgrokConfig();
// Check if the ngrok config file exists
if (!fs.existsSync(ngrokConfig)) {
throw new Error("No ngrok config file found");
}
// Read the ngrok config file
try {
const { authToken } = readNgrokConfig(ngrokConfig);
global.authToken = authToken;
} catch (err) {
console.error(err);
process.exit(1);
}
// Start ngrok
const proto = options?.proto ?? "http";
const addr = options?.port ? options.port : 3000;
try {
const url = await ngrok.connect({
authtoken: authToken,
proto,
addr,
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);
}
}
function getNgrokConfig() {
// Newer versions of ngrok
let ngrokConfig = os.homedir() + "/.ngrok2/ngrok.yml";
if (fs.existsSync(ngrokConfig)) {
return ngrokConfig;
}
// Alternative location for linux
ngrokConfig = os.homedir() + "/.config/ngrok/ngrok.yml";
if (fs.existsSync(ngrokConfig)) {
return ngrokConfig;
}
// MacOS
ngrokConfig = os.homedir() + "/Library/Application Support/ngrok/ngrok.yml";
if (fs.existsSync(ngrokConfig)) {
return ngrokConfig;
}
// Windows
ngrokConfig = os.homedir() + "/AppData/Local/ngrok/ngrok.yml";
if (fs.existsSync(ngrokConfig)) {
return ngrokConfig;
}
throw new Error("ngrok config file not found");
}
function readNgrokConfig(ngrokConfig) {
const data = fs.readFileSync(ngrokConfig, "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
throw new Error("Setup NGROK_AUTHTOKEN in your .env file");
}
return { authToken };
}
function findProjectFolder(filename = ".env") {
// Walk the path to find .env file... options?.env
let currentDir = __dirname;
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;
}
// Walk the path to find .env file...
function findDotEnv(filename = ".env") {
const currentDir = findProjectFolder(filename);
return `${currentDir}/${filename}`;
}
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(findDotEnv());
// 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(findDotEnv(), writeData);
}