feat(systemd-manager): Support sudo password and root detection in SystemdManager; add user/group support in services and templates; add tests and expand README

This commit is contained in:
2025-09-03 11:16:45 +00:00
parent 0ae3b9a5ec
commit 972688b8be
9 changed files with 552 additions and 78 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdaemon',
version: '2.0.9',
version: '2.1.0',
description: 'Start scripts as long running daemons and manage them.'
}

View File

@@ -1 +1,2 @@
export * from './smartdaemon.classes.smartdaemon.js';
export * from './smartdaemon.classes.service.js';

View File

@@ -8,6 +8,8 @@ export interface ISmartDaemonServiceConstructorOptions {
command: string;
workingDir: string;
version: string;
user?: string;
group?: string;
}
/**
@@ -33,6 +35,8 @@ export class SmartDaemonService implements ISmartDaemonServiceConstructorOptions
public command: string;
public workingDir: string;
public description: string;
public user?: string;
public group?: string;
public smartdaemonRef: SmartDaemon;

View File

@@ -6,13 +6,17 @@ import {
} from './smartdaemon.classes.service.js';
import { SmartDaemonSystemdManager } from './smartdaemon.classes.systemdmanager.js';
export interface ISmartDaemonOptions {
sudoPassword?: string;
}
export class SmartDaemon {
public templateManager: SmartDaemonTemplateManager;
public systemdManager: SmartDaemonSystemdManager;
constructor() {
constructor(optionsArg?: ISmartDaemonOptions) {
this.templateManager = new SmartDaemonTemplateManager(this);
this.systemdManager = new SmartDaemonSystemdManager(this);
this.systemdManager = new SmartDaemonSystemdManager(this, optionsArg?.sudoPassword);
}
/**

View File

@@ -31,13 +31,29 @@ export class SmartDaemonSystemdManager {
public smartsystem: plugins.smartsystem.Smartsystem;
public shouldExecute: boolean = false;
public isRoot: boolean = false;
public sudoPassword?: string;
constructor(smartdaemonRefArg: SmartDaemon) {
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() {
@@ -52,7 +68,28 @@ export class SmartDaemonSystemdManager {
public async execute(commandArg: string) {
if (await this.checkElegibility()) {
await this.smartshellInstance.exec(commandArg);
// 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;
}
}
}
@@ -101,18 +138,31 @@ export class SmartDaemonSystemdManager {
public async saveService(serviceArg: SmartDaemonService) {
if (await this.checkElegibility()) {
await plugins.smartfile.memory.toFs(
this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg),
SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name)
);
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()) {
await plugins.smartfile.fs.remove(
SmartDaemonSystemdManager.createServiceNameFromServiceName(serviceArg.name)
);
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
}
}
}

View File

@@ -1,4 +1,3 @@
import * as plugins from './smartdaemon.plugins.js';
import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
import { SmartDaemonService } from './smartdaemon.classes.service.js';
@@ -15,7 +14,9 @@ export class SmartDaemonTemplateManager {
# version: ${serviceArg.version}
# description: ${serviceArg.description}
# command: ${serviceArg.command}
# workingDir: ${serviceArg.workingDir}
# workingDir: ${serviceArg.workingDir}${serviceArg.user ? `
# user: ${serviceArg.user}` : ''}${serviceArg.group ? `
# group: ${serviceArg.group}` : ''}
# ---
[Unit]
Description=${serviceArg.description}
@@ -23,7 +24,9 @@ Requires=network.target
After=network.target
[Service]
Type=simple
Type=simple${serviceArg.user ? `
User=${serviceArg.user}` : ''}${serviceArg.group ? `
Group=${serviceArg.group}` : ''}
Environment=NODE_OPTIONS="--max_old_space_size=100"
ExecStart=/bin/bash -c "cd ${serviceArg.workingDir} && ${serviceArg.command}"
WorkingDirectory=${serviceArg.workingDir}