Files
smartdaemon/ts/smartdaemon.classes.systemdmanager.ts

198 lines
6.6 KiB
TypeScript

import * as plugins from './smartdaemon.plugins.js';
import * as paths from './smartdaemon.paths.js';
import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
import {
type ISmartDaemonServiceConstructorOptions,
SmartDaemonService,
} from './smartdaemon.classes.service.js';
export class SmartDaemonSystemdManager {
// STATIC
private static smartDaemonNamespace = 'smartdaemon';
public static createServiceNameFromServiceName(serviceNameArg: string) {
return `${SmartDaemonSystemdManager.smartDaemonNamespace}_${serviceNameArg}`;
}
public static createFileNameFromServiceName(serviceNameArg: string) {
return `${SmartDaemonSystemdManager.createServiceNameFromServiceName(serviceNameArg)}.service`;
}
public static createFilePathFromServiceName(serviceNameArg: string) {
return plugins.path.join(
paths.systemdDir,
SmartDaemonSystemdManager.createFileNameFromServiceName(serviceNameArg)
);
}
// INSTANCE
public smartdaemonRef: SmartDaemon;
public smartshellInstance: plugins.smartshell.Smartshell;
public smartsystem: plugins.smartsystem.Smartsystem;
public shouldExecute: boolean = false;
public isRoot: boolean = false;
public sudoPassword?: string;
constructor(smartdaemonRefArg: SmartDaemon, sudoPasswordArg?: string) {
this.smartdaemonRef = smartdaemonRefArg;
this.smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
this.smartsystem = new plugins.smartsystem.Smartsystem();
this.sudoPassword = sudoPasswordArg;
// Check if we're running as root
this.checkIsRoot().then(isRoot => {
this.isRoot = isRoot;
if (!isRoot) {
console.log('Not running as root. Sudo will be used for privileged operations.');
}
});
}
private async checkIsRoot(): Promise<boolean> {
const result = await this.smartshellInstance.exec('id -u');
return result.stdout.trim() === '0';
}
public async checkElegibility() {
if (await this.smartsystem.env.isLinuxAsync()) {
this.shouldExecute = true;
} else {
console.log('Smartdaemon can only be used on Linux systems! Refusing to set up a service.');
this.shouldExecute = false;
}
return this.shouldExecute;
}
public async execute(commandArg: string) {
if (await this.checkElegibility()) {
// Only use sudo if we're not root and command doesn't already have sudo
if (!this.isRoot && !commandArg.startsWith('sudo')) {
commandArg = `sudo ${commandArg}`;
if (this.sudoPassword) {
// Use interactive PTY mode for password input
const interactiveExec = await this.smartshellInstance.execInteractiveControlPty(commandArg);
await interactiveExec.sendLine(this.sudoPassword);
const result = await interactiveExec.finalPromise;
return result;
}
}
// Execute command (with or without sudo)
try {
await this.smartshellInstance.exec(commandArg);
} catch (error) {
if (!this.isRoot && error.message && error.message.includes('authentication')) {
throw new Error('Sudo authentication failed. Please configure passwordless sudo or provide a sudo password.');
}
throw error;
}
}
}
/**
* gets all services that are already present
*/
public async getServices() {
const existingServices: SmartDaemonService[] = [];
if (await this.checkElegibility()) {
const smartfmInstance = new plugins.smartfm.Smartfm({
fmType: 'yaml',
});
const availableServices = await plugins.smartfile.fs.fileTreeToObject(
paths.systemdDir,
'smartdaemon_*.service'
);
for (const serviceFile of availableServices) {
const data = smartfmInstance.parseFromComments('# ', serviceFile.contentBuffer.toString())
.data as ISmartDaemonServiceConstructorOptions;
const service = await SmartDaemonService.createFromOptions(this.smartdaemonRef, data);
service.alreadyExists = true;
existingServices.push(service);
}
}
return existingServices;
}
public async startService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
await this.execute(
`systemctl start ${SmartDaemonSystemdManager.createServiceNameFromServiceName(
serviceArg.name
)}`
);
}
}
public async stopService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
await this.execute(
`systemctl stop ${SmartDaemonSystemdManager.createServiceNameFromServiceName(
serviceArg.name
)}`
);
}
}
public async saveService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
const content = this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg);
const targetPath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
if (this.isRoot) {
// Direct write when running as root
await plugins.smartfile.memory.toFs(content, targetPath);
} else {
// Use sudo to write when not root
const tempPath = `/tmp/smartdaemon_${serviceArg.name}.service`;
await plugins.smartfile.memory.toFs(content, tempPath);
await this.execute(`mv ${tempPath} ${targetPath}`); // execute() will add sudo
await this.execute(`chmod 644 ${targetPath}`); // execute() will add sudo
}
}
}
public async deleteService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
const filePath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
if (this.isRoot) {
await plugins.smartfile.fs.remove(filePath);
} else {
await this.execute(`rm ${filePath}`); // execute() will add sudo
}
}
}
public async enableService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
await this.saveService(serviceArg);
if (serviceArg.alreadyExists) {
await this.execute(`systemctl daemon-reload`);
}
await this.execute(
`systemctl enable ${SmartDaemonSystemdManager.createServiceNameFromServiceName(
serviceArg.name
)}`
);
}
}
public async disableService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
await this.execute(
`systemctl disable ${SmartDaemonSystemdManager.createServiceNameFromServiceName(
serviceArg.name
)}`
);
}
}
public async reload() {
if (await this.checkElegibility()) {
await this.execute(`systemctl daemon-reload`);
}
}
}