From e29f6fed22f2380418e8a588a745cdf6a8142d4e Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Thu, 24 Jul 2025 13:56:21 +0200 Subject: [PATCH 1/5] start stop plw --- src/cli.ts | 15 +++++- src/plw/index.ts | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/plw/index.ts diff --git a/src/cli.ts b/src/cli.ts index 1aa5716..f309d8f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -import { program, Command } from "commander"; +import { program, Command, createCommand } from "commander"; import { version } from "./version"; import { createEnvironment, @@ -15,6 +15,8 @@ import { Config, loadConfig, saveConfig } from "./config"; import { promptUser, resolveTestTargetId } from "./helpers"; import { runDebugtopus } from "./debugtopus"; +import { startPrivateLocationWorker, stopPLW } from "./plw"; + const createCommandWithCommonOptions = (command: string): Command => { return program .command(command) @@ -233,5 +235,16 @@ export const buildCmd = (): Command => { .requiredOption("-e, --environment-id ", "Environment ID") .option("-t, --test-target-id ", "Test target ID") .action(deleteEnvironment); + + program.command("start-private-location") + .description("Start a private location worker") + .option("-n, --name ", "Location name") + .option("-u, --username ", "Proxy user") + .option("-p, --password ", "Proxy password") + .action(startPrivateLocationWorker); + + program.command("stop-private-location") + .description("Stop a private location worker") + .action(stopPLW) return program; }; diff --git a/src/plw/index.ts b/src/plw/index.ts new file mode 100644 index 0000000..9e531a8 --- /dev/null +++ b/src/plw/index.ts @@ -0,0 +1,134 @@ + +import { randomUUID } from "crypto"; +import { loadConfig } from "../config"; +import { spawn, ChildProcess } from 'child_process'; +import { createInterface } from 'readline'; + +interface StreamResult { + lines: string[]; + detached: boolean; + code: number | null; + signal?: NodeJS.Signals | null; +} + +const spawnAndStreamLines = async ( + command: string, + args: string[] = [], + maxLines: number = 20 +): Promise => { + console.log(`Spawning command: ${command} ${args.join(" ")}`); + return new Promise((resolve, reject) => { + const child: ChildProcess = spawn(command, args, { + stdio: ['ignore', 'pipe', 'pipe'], + detached: true, + shell: true, + env: process.env + }); + + if (!child.stdout) { + reject(new Error('Failed to create stdout pipe')); + return; + } + + const rl = createInterface({ + input: child.stdout, + crlfDelay: Infinity + }); + + const lines: string[] = []; + let lineCount = 0; + + rl.on('line', (line: string) => { + lines.push(line); + console.log(`-- ${line}`); + lineCount++; + + if (lineCount >= maxLines) { + + rl.close(); + child.unref(); + child.stdout?.destroy(); + child.stderr?.destroy(); + console.log("resolve on(line)"); + resolve({ lines, detached: true, code: null, signal: null }); + } + }); + + rl.on('close', () => { + if (lineCount < maxLines) { + // Process ended before reaching max lines + console.log("resolve on(close)"); + resolve({ lines, detached: false, code: null, signal: null }); + } + }); + + child.on('error', (error) => { + rl.close(); + reject(error); + }); + + child.on('exit', (code, signal) => { + if (lineCount < maxLines) { + console.log(`Process exited with code ${code}, signal ${signal}`); + } + console.log("resolve on(exit)"); + resolve({ lines, detached: false, code, signal }); + }); + }); +} + +const checkDockerDaemon = (): Promise => { + return new Promise((resolve) => { + const child = spawn('docker', ['info'], { stdio: 'pipe' }); + + child.on('exit', (code) => { + resolve(code === 0); + }); + }); +} + +const createDockerCommand = (options: { + apiKey: string; + name?: string; + username?: string; + password?: string; +}): string => { + return `docker run --rm --name PLW -e PLW_NAME=${options.name} -e PROXY_USER=${options.username} -e PROXY_PASS=${options.password} -e APIKEY=${options.apiKey} eu.gcr.io/octomind-dev/plw:latest`; +}; + +export const startPrivateLocationWorker = async (options: { name?: string, username?: string, password?: string, apikey: string }) => { + + const name = options.name || "default-plw"; + const username = options.username || randomUUID().replace(/-/g, ''); + const password = options.password || randomUUID().replace(/-/g, ''); + + const { apiKey } = await loadConfig(); + if (!apiKey) { + throw new Error( + "API key is required. Please configure it first by running 'octomind init'", + ); + } + let command = createDockerCommand({ name, username, password, apiKey }); + + if( !await checkDockerDaemon()) { + console.error("Docker daemon is not running. Please start Docker and try again."); + return; + } + + console.log(`executing command : '${command}'`); + const args = command.split(" "); + const result = await spawnAndStreamLines(args[0], args.slice(1), 10); + console.log(`Captured ${result.lines.length} lines`); + console.log(`Process detached: ${result.detached}`); +}; + +export const stopPLW = async () : Promise => { + const command = 'docker stop PLW'; + const args = command.split(" "); + const result = await spawnAndStreamLines(args[0], args.slice(1), 10); + if(result.code === 0) { + console.log("Private Location Worker stopped successfully."); + } else { + console.error(`Failed to stop Private Location Worker. Exit code: ${result.code}`); + } +} \ No newline at end of file From b1c87dda4a950c6a6ab34953726f3be17bc31e56 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Thu, 24 Jul 2025 13:58:03 +0200 Subject: [PATCH 2/5] remove log --- src/plw/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/plw/index.ts b/src/plw/index.ts index 9e531a8..e194a7c 100644 --- a/src/plw/index.ts +++ b/src/plw/index.ts @@ -49,19 +49,10 @@ const spawnAndStreamLines = async ( child.unref(); child.stdout?.destroy(); child.stderr?.destroy(); - console.log("resolve on(line)"); resolve({ lines, detached: true, code: null, signal: null }); } }); - rl.on('close', () => { - if (lineCount < maxLines) { - // Process ended before reaching max lines - console.log("resolve on(close)"); - resolve({ lines, detached: false, code: null, signal: null }); - } - }); - child.on('error', (error) => { rl.close(); reject(error); @@ -71,7 +62,6 @@ const spawnAndStreamLines = async ( if (lineCount < maxLines) { console.log(`Process exited with code ${code}, signal ${signal}`); } - console.log("resolve on(exit)"); resolve({ lines, detached: false, code, signal }); }); }); From 85b2a9c3951ed54941fdc02381ed9e50dcf5119d Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Thu, 24 Jul 2025 14:14:29 +0200 Subject: [PATCH 3/5] lint --- src/plw/index.ts | 70 ++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/plw/index.ts b/src/plw/index.ts index e194a7c..1f7eaa4 100644 --- a/src/plw/index.ts +++ b/src/plw/index.ts @@ -1,8 +1,7 @@ - import { randomUUID } from "crypto"; import { loadConfig } from "../config"; -import { spawn, ChildProcess } from 'child_process'; -import { createInterface } from 'readline'; +import { spawn, ChildProcess } from "child_process"; +import { createInterface } from "readline"; interface StreamResult { lines: string[]; @@ -12,39 +11,38 @@ interface StreamResult { } const spawnAndStreamLines = async ( - command: string, - args: string[] = [], - maxLines: number = 20 + command: string, + args: string[] = [], + maxLines: number = 20, ): Promise => { console.log(`Spawning command: ${command} ${args.join(" ")}`); return new Promise((resolve, reject) => { const child: ChildProcess = spawn(command, args, { - stdio: ['ignore', 'pipe', 'pipe'], + stdio: ["ignore", "pipe", "pipe"], detached: true, - shell: true, - env: process.env + shell: true, + env: process.env, }); if (!child.stdout) { - reject(new Error('Failed to create stdout pipe')); + reject(new Error("Failed to create stdout pipe")); return; } const rl = createInterface({ input: child.stdout, - crlfDelay: Infinity + crlfDelay: Infinity, }); const lines: string[] = []; let lineCount = 0; - rl.on('line', (line: string) => { + rl.on("line", (line: string) => { lines.push(line); console.log(`-- ${line}`); lineCount++; if (lineCount >= maxLines) { - rl.close(); child.unref(); child.stdout?.destroy(); @@ -53,29 +51,29 @@ const spawnAndStreamLines = async ( } }); - child.on('error', (error) => { + child.on("error", (error) => { rl.close(); reject(error); }); - child.on('exit', (code, signal) => { + child.on("exit", (code, signal) => { if (lineCount < maxLines) { console.log(`Process exited with code ${code}, signal ${signal}`); } resolve({ lines, detached: false, code, signal }); }); }); -} +}; const checkDockerDaemon = (): Promise => { return new Promise((resolve) => { - const child = spawn('docker', ['info'], { stdio: 'pipe' }); - - child.on('exit', (code) => { + const child = spawn("docker", ["info"], { stdio: "pipe" }); + + child.on("exit", (code) => { resolve(code === 0); }); }); -} +}; const createDockerCommand = (options: { apiKey: string; @@ -86,11 +84,15 @@ const createDockerCommand = (options: { return `docker run --rm --name PLW -e PLW_NAME=${options.name} -e PROXY_USER=${options.username} -e PROXY_PASS=${options.password} -e APIKEY=${options.apiKey} eu.gcr.io/octomind-dev/plw:latest`; }; -export const startPrivateLocationWorker = async (options: { name?: string, username?: string, password?: string, apikey: string }) => { - +export const startPrivateLocationWorker = async (options: { + name?: string; + username?: string; + password?: string; + apikey: string; +}) => { const name = options.name || "default-plw"; - const username = options.username || randomUUID().replace(/-/g, ''); - const password = options.password || randomUUID().replace(/-/g, ''); + const username = options.username || randomUUID().replace(/-/g, ""); + const password = options.password || randomUUID().replace(/-/g, ""); const { apiKey } = await loadConfig(); if (!apiKey) { @@ -98,10 +100,12 @@ export const startPrivateLocationWorker = async (options: { name?: string, usern "API key is required. Please configure it first by running 'octomind init'", ); } - let command = createDockerCommand({ name, username, password, apiKey }); + const command = createDockerCommand({ name, username, password, apiKey }); - if( !await checkDockerDaemon()) { - console.error("Docker daemon is not running. Please start Docker and try again."); + if (!(await checkDockerDaemon())) { + console.error( + "Docker daemon is not running. Please start Docker and try again.", + ); return; } @@ -112,13 +116,15 @@ export const startPrivateLocationWorker = async (options: { name?: string, usern console.log(`Process detached: ${result.detached}`); }; -export const stopPLW = async () : Promise => { - const command = 'docker stop PLW'; +export const stopPLW = async (): Promise => { + const command = "docker stop PLW"; const args = command.split(" "); const result = await spawnAndStreamLines(args[0], args.slice(1), 10); - if(result.code === 0) { + if (result.code === 0) { console.log("Private Location Worker stopped successfully."); } else { - console.error(`Failed to stop Private Location Worker. Exit code: ${result.code}`); + console.error( + `Failed to stop Private Location Worker. Exit code: ${result.code}`, + ); } -} \ No newline at end of file +}; From 5b0eb7d986aad45a788c3e5662c8d1b6bb90d2d2 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Thu, 24 Jul 2025 16:30:59 +0200 Subject: [PATCH 4/5] Update src/cli.ts Co-authored-by: mxlnk --- src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index e954684..92e35d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -import { program, Command, createCommand } from "commander"; +import { program, Command } from "commander"; import { version } from "./version"; import { createDiscovery, From 9e9fd6f6f2361983752c437cb15dc6d1316dfed9 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Thu, 24 Jul 2025 16:32:17 +0200 Subject: [PATCH 5/5] remove log --- src/plw/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plw/index.ts b/src/plw/index.ts index 1f7eaa4..dcbd2dd 100644 --- a/src/plw/index.ts +++ b/src/plw/index.ts @@ -15,7 +15,6 @@ const spawnAndStreamLines = async ( args: string[] = [], maxLines: number = 20, ): Promise => { - console.log(`Spawning command: ${command} ${args.join(" ")}`); return new Promise((resolve, reject) => { const child: ChildProcess = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"],