* Copyright (C) 2020 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
* session in a transient systemd service unit. See
* examples/long-running-process/README.md for details.
*
* The unit will run as root, on the system systemd manager, so that every privileged
* Cockpit session shares the same unit. The same approach works in principle on
* the user's systemd instance, but the current code does not support that as it
* is not a common use case for Cockpit.
*/
const O_SD_OBJ = "/org/freedesktop/systemd1";
const I_SD_MGR = "org.freedesktop.systemd1.Manager";
const I_SD_UNIT = "org.freedesktop.systemd1.Unit";
const I_DBUS_PROP = "org.freedesktop.DBus.Properties";
export const ProcessState = {
INIT: 'init',
STOPPED: 'stopped',
RUNNING: 'running',
FAILED: 'failed',
};
export class LongRunningProcess {
* updateCallback: function that gets called whenever the state changed; first and only
* argument is `this` LongRunningProcess instance.
*/
constructor(serviceName, updateCallback) {
this.systemdClient = cockpit.dbus("org.freedesktop.systemd1", { superuser: "require" });
this.serviceName = serviceName;
this.updateCallback = updateCallback;
this._setState(ProcessState.INIT);
this.startTimestamp = null;
this.terminated = false;
this.systemdClient.subscribe({ interface: I_SD_MGR, member: "JobNew" }, (path, iface, signal, args) => {
if (args[2] === this.serviceName)
this._checkState();
});
this._checkState();
}
* This runs as root, thus will be shared with all privileged Cockpit sessions.
* Return cockpit.spawn promise. You need to handle exceptions, but not success.
* @param {string[]} argv array starting with executable and all of the arguments to pass to it
* @param {object} options same properties as cockpit-spawn https://docs.cockpit-project.org/cockpit-guide/latest/guide/cockpit-spawn.html/cockpit-spawn.html
* @param {string[]} runArgs arguments to provide to systemd-run itself, such as `--setenv`
*/
run(argv, options, runArgs = []) {
if (this.state !== ProcessState.STOPPED && this.state !== ProcessState.FAILED)
throw new Error(`cannot start LongRunningProcess in state ${this.state}`);
return cockpit.spawn(
[
"systemd-run",
"--unit", this.serviceName,
"--service-type=oneshot",
"--no-block",
...runArgs,
"--"
].concat(argv),
{ superuser: "require", err: "message", ...options }
);
}
terminate() {
if (this.state !== ProcessState.RUNNING && this.state !== ProcessState.FAILED)
throw new Error(`cannot terminate LongRunningProcess in state ${this.state}`);
* happen with `systemd-run -p SuccessExitStatus=0`, but that does not yet work on older
* OSes with systemd โค 241 So let checkState() know that a failure is due to termination. */
this.terminated = true;
return this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "StopUnit", [this.serviceName, "replace"], { type: "ss" });
}
reset() {
if (this.state === ProcessState.FAILED)
this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "ResetFailedUnit", [this.serviceName], { type: "s" });
else
throw new Error(`cannot reset LongRunningProcess in state ${this.state}`);
}
* below are internal private methods
*/
_setState(state) {
if (state === this.state)
return;
this.state = state;
this.terminated = false;
if (this.updateCallback)
this.updateCallback(this);
}
_setStateFromProperties(activeState, stateChangeTimestamp) {
switch (activeState) {
case 'activating':
this.startTimestamp = stateChangeTimestamp;
this._setState(ProcessState.RUNNING);
break;
case 'failed':
this.startTimestamp = null;
if (this.terminated) {
this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "ResetFailedUnit", [this.serviceName], { type: "s" });
} else {
this._setState(ProcessState.FAILED);
}
break;
case 'inactive':
this._setState(ProcessState.STOPPED);
break;
case 'deactivating':
break;
default:
throw new Error(`unexpected state of unit ${this.serviceName}: ${activeState}`);
}
}
_checkState() {
this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "GetUnit", [this.serviceName], { type: "s" })
.then(([unitObj]) => {
* we may get an inactive unit here; watch for state changes. This will also update
* the UI if the unit stops. */
this.subscription = this.systemdClient.subscribe(
{ interface: I_DBUS_PROP, member: "PropertiesChanged" },
(path, iface, signal, args) => {
if (path === unitObj && args[1].ActiveState && args[1].StateChangeTimestamp)
this._setStateFromProperties(args[1].ActiveState.v, args[1].StateChangeTimestamp.v);
});
this.systemdClient.call(unitObj, I_DBUS_PROP, "GetAll", [I_SD_UNIT], { type: "s" })
.then(([props]) => this._setStateFromProperties(props.ActiveState.v, props.StateChangeTimestamp.v))
.catch(ex => {
throw new Error(`unexpected failure of GetAll(${unitObj}): ${ex.toString()}`);
});
})
.catch(ex => {
if (ex.name === "org.freedesktop.systemd1.NoSuchUnit") {
if (this.subscription) {
this.subscription.remove();
this.subscription = null;
}
this._setState(ProcessState.STOPPED);
} else {
throw new Error(`unexpected failure of GetUnit(${this.serviceName}): ${ex.toString()}`);
}
});
}
}