137 lines
3.1 KiB
TypeScript
137 lines
3.1 KiB
TypeScript
import EventEmitter from 'events';
|
|
import { IProcess, IProcessLauncher, ProcessLaunchOptions } from '@computernewb/superqemu';
|
|
import { execaCommand } from 'execa';
|
|
import { Readable, Writable } from 'stream';
|
|
import { CGroup } from '../util/cgroup.js';
|
|
|
|
export interface CgroupLimits {
|
|
cpuUsageMax?: number;
|
|
runOnCpus?: number[];
|
|
limitProcess?: boolean;
|
|
}
|
|
|
|
interface CGroupValue {
|
|
controller: string;
|
|
key: string;
|
|
value: string;
|
|
}
|
|
|
|
function MakeValuesFromLimits(limits: CgroupLimits): CGroupValue[] {
|
|
let option_array = [];
|
|
|
|
if (limits.cpuUsageMax) {
|
|
// cpu.max
|
|
option_array.push({
|
|
controller: 'cpu',
|
|
key: 'max',
|
|
value: `${(limits.cpuUsageMax / 100) * 100000} 100000`
|
|
});
|
|
}
|
|
|
|
if(limits.runOnCpus) {
|
|
// Make sure a CPU is not specified more than once. Bit hacky but oh well
|
|
let unique = [...new Set(limits.runOnCpus)];
|
|
option_array.push({
|
|
controller: 'cpuset',
|
|
key: 'cpus',
|
|
value: `${unique.join(',')}`
|
|
});
|
|
}
|
|
|
|
return option_array;
|
|
}
|
|
|
|
// A process automatically placed in a given cgroup.
|
|
class CGroupLimitedProcess extends EventEmitter implements IProcess {
|
|
private process;
|
|
stdin: Writable | null = null;
|
|
stdout: Readable | null = null;
|
|
stderr: Readable | null = null;
|
|
private root_cgroup: CGroup;
|
|
private cgroup: CGroup;
|
|
private id;
|
|
private limits;
|
|
|
|
constructor(cgroup_root: CGroup, id: string, limits: CgroupLimits, command: string, opts?: ProcessLaunchOptions) {
|
|
super();
|
|
this.root_cgroup = cgroup_root;
|
|
this.cgroup = cgroup_root.GetSubgroup(id);
|
|
this.id = id;
|
|
this.limits = limits;
|
|
|
|
if(!this.limits.limitProcess)
|
|
this.limits.limitProcess = false;
|
|
|
|
this.process = execaCommand(command, opts);
|
|
|
|
this.stdin = this.process.stdin;
|
|
this.stdout = this.process.stdout;
|
|
this.stderr = this.process.stderr;
|
|
|
|
let self = this;
|
|
this.process.on('spawn', () => {
|
|
self.initCgroup();
|
|
|
|
if(self.limits.limitProcess) {
|
|
// it should have one!
|
|
self.cgroup.AttachProcess(self.process.pid!);
|
|
}
|
|
self.emit('spawn');
|
|
});
|
|
|
|
this.process.on('exit', (code) => {
|
|
self.emit('exit', code);
|
|
});
|
|
}
|
|
|
|
initCgroup() {
|
|
// Set cgroup keys.
|
|
for(const val of MakeValuesFromLimits(this.limits)) {
|
|
let controller = this.cgroup.GetController(val.controller);
|
|
controller.WriteValue(val.key, val.value);
|
|
}
|
|
}
|
|
|
|
kill(signal?: number | NodeJS.Signals): boolean {
|
|
return this.process.kill(signal);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.stdin = null;
|
|
this.stdout = null;
|
|
this.stderr = null;
|
|
|
|
this.root_cgroup.DeleteSubgroup(this.id);
|
|
this.process.removeAllListeners();
|
|
this.removeAllListeners();
|
|
}
|
|
}
|
|
|
|
export class QemuResourceLimitedLauncher implements IProcessLauncher {
|
|
private limits;
|
|
private name;
|
|
private root;
|
|
public group;
|
|
|
|
constructor(name: string, limits: CgroupLimits) {
|
|
this.root = CGroup.Self();
|
|
|
|
// Make sure
|
|
if(limits.runOnCpus) {
|
|
this.root.InitControllers(true);
|
|
} else {
|
|
this.root.InitControllers(false);
|
|
}
|
|
|
|
this.name = name;
|
|
this.limits = limits;
|
|
|
|
// XXX figure something better out
|
|
this.group = this.root.GetSubgroup(this.name);
|
|
}
|
|
|
|
launch(command: string, opts?: ProcessLaunchOptions | undefined): IProcess {
|
|
return new CGroupLimitedProcess(this.root, this.name, this.limits, command, opts);
|
|
}
|
|
}
|