amaran-light-cli
Version:
Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.
121 lines • 4.24 kB
JavaScript
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import WebSocket from 'ws';
const execAsync = promisify(exec);
// Parse lsof output lines for ports bound by processes starting with "amaran"
export function parseAmaranPorts(lsofOutput) {
const results = [];
const lines = lsofOutput.split(/\r?\n/);
for (const line of lines) {
// Trim and quickly filter
const trimmed = line.trim();
if (!trimmed)
continue;
// lsof table rows typically start with COMMAND name in column 1
// We only care about processes that start with "amaran" (case-insensitive)
const startsWithAmaran = /^amaran/i.test(trimmed);
if (!startsWithAmaran)
continue;
// Look for localhost binds and capture port
// Examples (macOS):
// amaran 1234 mac 10u IPv4 ... TCP 127.0.0.1:60124 (LISTEN)
// amaran 1234 mac 11u IPv6 ... TCP [::1]:60124 (LISTEN)
// amaran 1234 mac 12u IPv4 ... TCP 127.0.0.1:60124->127.0.0.1:65000 (ESTABLISHED)
let match = null;
let iface = null;
match = trimmed.match(/127\.0\.0\.1:(\d+)/);
if (match) {
iface = '127.0.0.1';
}
else {
match = trimmed.match(/\[::1\]:(\d+)/);
if (match) {
iface = '::1';
}
}
if (!match || !iface)
continue;
const port = parseInt(match[1], 10);
if (!Number.isFinite(port))
continue;
// Prefer LISTEN sockets, but also record ESTABLISHED as fallback
const isListen = /\(LISTEN\)/.test(trimmed);
// Ensure uniqueness (port, iface)
if (!results.some((r) => r.port === port && r.iface === iface)) {
results.push({ port, process: trimmed.split(/\s+/)[0], iface });
}
// If it's a LISTEN entry, push it to the front to prioritize
if (isListen) {
const idx = results.findIndex((r) => r.port === port && r.iface === iface);
if (idx > 0) {
const [item] = results.splice(idx, 1);
results.unshift(item);
}
}
}
return results;
}
async function probeWebSocket(url, timeoutMs = 1200) {
return new Promise((resolve) => {
let settled = false;
const ws = new WebSocket(url);
const timer = setTimeout(() => {
if (!settled) {
settled = true;
try {
ws.terminate();
}
catch {
// Ignore termination errors
}
resolve(false);
}
}, timeoutMs);
ws.on('open', () => {
if (!settled) {
settled = true;
clearTimeout(timer);
try {
ws.close();
}
catch {
// Ignore close errors
}
resolve(true);
}
});
ws.on('error', () => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve(false);
}
});
});
}
export async function discoverLocalWebSocket(preferredHost = '127.0.0.1', debug = false) {
try {
const { stdout } = await execAsync('lsof -i -P -n');
const candidates = parseAmaranPorts(stdout);
if (debug) {
console.log('[discovery] lsof candidates:', candidates);
}
// Try LISTEN candidates first (we unshifted those), then others
for (const c of candidates) {
const host = preferredHost === 'localhost' ? 'localhost' : c.iface === '::1' ? '127.0.0.1' : '127.0.0.1';
const url = `ws://${host}:${c.port}`;
const ok = await probeWebSocket(url, 1200);
if (ok) {
return { url, port: c.port, process: c.process };
}
}
}
catch (err) {
if (debug) {
console.log('[discovery] lsof failed:', err);
}
}
return null;
}
export default discoverLocalWebSocket;
//# sourceMappingURL=discovery.js.map