// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.
import { spawn } from 'child_process';
import { log } from './log.js';
export const createPlayerCommon = (
name, // name of player, for messages
cmd, // program to run as process
options, // arguments to cmd
env, // optional, environment for process
id, // optional id string, generated if not provided
filesP // optional, [promise that resolves to] array of additional arguments
) => {
id = id || Math.random().toString(36).slice(2, 8);
let proc = null;
let stdoutBuffered = '';
let paused = false, pauseTarget = false, readySet = false;
let killTimeout = null;
// Pause interface with a bit of intelligence to prevent erratic behavior.
const callTogglePauseTo = (intf, target) => {
if (! intf.togglePauseTo) { return } // none to call
if (target === pauseTarget) { return } // has or is going to the desired state
if (paused !== pauseTarget) { // state change underway, retry soon
return new Promise((resolve, reject) => {
setTimeout(() => { resolve(callTogglePauseTo(intf, target)) }, 100)
});
}
pauseTarget = target;
return intf.togglePauseTo(target).then(() => {
paused = target;
}).catch(() => { });
}
const exitWithMessage = (message) => {
runExitCallbacks(message).then(() => {
cancelReady({ message: message, defunct: true });
provideFinish({ message: message });
});
}
const onExitCallbacks = [];
const runExitCallbacks = async (statusOrMessage) => {
for (const f of onExitCallbacks) { f(statusOrMessage) }
}
const exitWithStatus = (status) => {
runExitCallbacks(status).then(() => {
cancelReady({ status: status, message: "Exited with status "+status, defunct: true });
provideFinish({ status: status, message: "Exited with status "+status });
});
}
const intf = {
name: name,
id: id,
// --------------------------------------------------------------------
// TO BE OVERRIDDEN FOR SPECIALIZED BEHAVIOR
// String or regexp to find in process output before setting to ready.
readyPattern: "",
// Processes part of bufferedString, returns the number of chars
// gobbled. If there isn't a complete thing to process, return 0. By
// default, if ready is not yet set, checks for readyPattern, and if
// found sets ready and returns the end position of the pattern,
// otherwise sends the string to the process standarrd output and
// returns the length.
processStdout: (bufferedString) => {
process.stdout.write(bufferedString);
return bufferedString.length;
},
// Handle output to stderr.
noteStderr: (buf) => {
log.error(buf.toString());
},
// Nicely ask the process to quit. May return promise that resolves
// when the request has been successfully sent and rejects if it could
// not be sent.
niceQuit: () => {
intf.writeProcStdin('quit\n').catch(() => { log.debug("Nice quit failed") });
},
// Set pause to target status (true or false), which is always supposed
// to be different than the current status. May return promise that
// resolves when the request has been successfully sent and rejects if
// it could not be sent.
togglePauseTo: undefined,
// Add callback to be invoked when underlying process exits. Callback
// gets either exit status (number) or message as argument.
onExit: (f) => onExitCallbacks.push(f),
// END TO BE OVERRIDDEN FOR SPECIALIZED BEHAVIOR.
// --------------------------------------------------------------------
ready: () => {
cancelReady = rejectReady;
return readyPromise;
},
finish: () => finishPromise,
writeProcStdin: (s) => {
return new Promise((resolve, reject) => {
proc.stdin.write(s, err => {
if (err) { reject(err) }
else { resolve() }
});
});
},
pausedStatus: () => pauseTarget,
// Subset of interface that can be accessed via http.
httpCallable: {
// Tells the process to quit with escalating determination, starting
// with call to niceQuit. Does not wait for the process to exit (the
// start promise resolves when the process exits).
quit: () => {
if (proc) {
intf.niceQuit(proc);
killTimeout = setTimeout(
() => {
if (proc) {
proc.kill('SIGTERM');
killTimeout = setTimeout(() => { if (proc) { proc.kill('SIGKILL') } }, 5000);
}
},
3000);
}
return intf.finish();
},
pause: () => callTogglePauseTo(intf, true),
play: () => callTogglePauseTo(intf, false),
toggle_pause: () => callTogglePauseTo(intf, !pauseTarget),
}
};
let cancelReady = () => { intf.ready = () => Promise.reject() }
let rejectReady;
const readyPromise = new Promise((resolve, reject) => {
intf.setReady = () => {
readySet = true;
resolve(intf);
}
rejectReady = reject;
});
let provideFinish;
const finishPromise = new Promise(resolve => { provideFinish = resolve });
// Start the process.
Promise.resolve(filesP || []).then(files => {
const args = options.concat(files);
log.debug(cmd+" "+args.join(" "));
proc = spawn(cmd, args, env);
proc.stdin.on('error', err => { exitWithMessage(err ? err.toString() : "stdin error") });
proc.stderr.on('error', err => { exitWithMessage(err ? err.toString() : "stderr error") });
proc.stdout.on('data', buf => {
stdoutBuffered += buf.toString();
if (!readySet) {
const p = intf.readyPattern;
if (typeof p === 'string') {
const m = stdoutBuffered.indexOf(p);
if (m >= 0) {
intf.setReady();
stdoutBuffered = stdoutBuffered.slice(m + p.length);;
}
} else {
const m = p.exec(stdoutBuffered);
if (m) {
intf.setReady();
stdoutBuffered = stdoutBuffered.slice(m.index + m[0].length);
}
}
}
while (true) {
const endProcessed = intf.processStdout(stdoutBuffered);
if (endProcessed === 0) { break; }
stdoutBuffered = stdoutBuffered.slice(endProcessed);
}
});
proc.stderr.on('data', buf => {
intf.noteStderr(buf);
});
proc.on('exit', (status, signal) => {
log.debug("exit "+intf.name+": "+status+"/"+signal);
proc = null;
clearTimeout(killTimeout);
if (status !== null) { exitWithStatus(status) }
else { exitWithMessage("Terminated by signal "+signal) }
});
}).catch(err => {
log.debug(err+"\n"+(err.stack || "(No stack)"));
exitWithMessage(err && err.toString());
});
return intf;
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home