@push.rocks/smartdaemon 2.22.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './smartdaemon.plugins.js';
2
2
  import * as paths from './smartdaemon.paths.js';
3
3
  import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
4
+ import { SmartDaemonCommandError } from './smartdaemon.classes.commanderror.js';
4
5
  import {
5
6
  type ISmartDaemonServiceConstructorOptions,
6
7
  SmartDaemonService,
@@ -31,8 +32,10 @@ export class SmartDaemonSystemdManager {
31
32
  public smartFs: plugins.smartfs.SmartFs;
32
33
 
33
34
  public shouldExecute: boolean = false;
35
+ /** Whether this process runs as root; set by the first privileged operation's check. */
34
36
  public isRoot: boolean = false;
35
37
  public sudoPassword?: string;
38
+ private rootStatus?: Promise<void>;
36
39
 
37
40
  constructor(smartdaemonRefArg: SmartDaemon, sudoPasswordArg?: string) {
38
41
  this.smartdaemonRef = smartdaemonRefArg;
@@ -41,14 +44,6 @@ export class SmartDaemonSystemdManager {
41
44
  });
42
45
  this.smartFs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
43
46
  this.sudoPassword = sudoPasswordArg;
44
-
45
- // Check if we're running as root
46
- this.checkIsRoot().then(isRoot => {
47
- this.isRoot = isRoot;
48
- if (!isRoot) {
49
- console.log('Not running as root. Sudo will be used for privileged operations.');
50
- }
51
- });
52
47
  }
53
48
 
54
49
  private async checkIsRoot(): Promise<boolean> {
@@ -56,6 +51,23 @@ export class SmartDaemonSystemdManager {
56
51
  return result.stdout.trim() === '0';
57
52
  }
58
53
 
54
+ /** Checks once whether this process runs as root and sets `isRoot`; later calls reuse that check. */
55
+ private ensureRootStatus(): Promise<void> {
56
+ if (!this.rootStatus) {
57
+ this.rootStatus = this.checkIsRoot().then((isRoot) => {
58
+ this.isRoot = isRoot;
59
+ if (!isRoot) {
60
+ console.log('Not running as root. Sudo will be used for privileged operations.');
61
+ }
62
+ });
63
+ // A failed check is not memoized, so the next privileged operation asks again.
64
+ this.rootStatus.catch(() => {
65
+ this.rootStatus = undefined;
66
+ });
67
+ }
68
+ return this.rootStatus;
69
+ }
70
+
59
71
  public async checkElegibility() {
60
72
  if (plugins.os.platform() === 'linux') {
61
73
  this.shouldExecute = true;
@@ -66,31 +78,61 @@ export class SmartDaemonSystemdManager {
66
78
  return this.shouldExecute;
67
79
  }
68
80
 
69
- public async execute(commandArg: string) {
70
- if (await this.checkElegibility()) {
71
- // Only use sudo if we're not root and command doesn't already have sudo
72
- if (!this.isRoot && !commandArg.startsWith('sudo')) {
73
- commandArg = `sudo ${commandArg}`;
74
-
75
- if (this.sudoPassword) {
76
- // Use interactive PTY mode for password input
77
- const interactiveExec = await this.smartshellInstance.execInteractiveControlPty(commandArg);
78
- await interactiveExec.sendLine(this.sudoPassword);
79
- const result = await interactiveExec.finalPromise;
80
- return result;
81
- }
82
- }
83
-
84
- // Execute command (with or without sudo)
85
- try {
86
- await this.smartshellInstance.exec(commandArg);
87
- } catch (error) {
88
- if (!this.isRoot && error instanceof Error && error.message.includes('authentication')) {
89
- throw new Error('Sudo authentication failed. Please configure passwordless sudo or provide a sudo password.');
90
- }
91
- throw error;
92
- }
81
+ /**
82
+ * Runs a command, through sudo when this process is not root and the command does not start with
83
+ * `sudo` already. With a `sudoPassword`, sudo reads it from standard input (`sudo -k -S -p ''`):
84
+ * it never reaches argv, the environment or a terminal. Rejects with `SmartDaemonCommandError`
85
+ * when the command does not exit 0.
86
+ */
87
+ public async execute(commandArg: string): Promise<plugins.smartshell.IExecResult | undefined> {
88
+ if (!(await this.checkElegibility())) {
89
+ return undefined;
90
+ }
91
+ await this.ensureRootStatus();
92
+ if (this.isRoot || commandArg.startsWith('sudo')) {
93
+ return await this.runChecked(commandArg);
94
+ }
95
+ if (!this.sudoPassword) {
96
+ return await this.runChecked(`sudo ${commandArg}`);
97
+ }
98
+ // -k ignores cached credentials, so sudo reads the password line whenever its policy asks for
99
+ // one; ending stdin right after it makes a rejected password fail instead of waiting for a retry.
100
+ const sudoCommand = `sudo -k -S -p '' -- ${commandArg}`;
101
+ const interactiveExec = await this.smartshellInstance.execInteractiveControl(sudoCommand);
102
+ let writeError: unknown;
103
+ try {
104
+ await interactiveExec.sendLine(this.sudoPassword);
105
+ } catch (error) {
106
+ // sudo can exit before reading stdin (EPIPE); its exit status below decides the outcome.
107
+ writeError = error;
108
+ }
109
+ interactiveExec.endInput();
110
+ const result = await interactiveExec.finalPromise;
111
+ this.assertSucceeded(sudoCommand, result, writeError);
112
+ return result;
113
+ }
114
+
115
+ private async runChecked(commandArg: string): Promise<plugins.smartshell.IExecResult> {
116
+ const result = await this.smartshellInstance.exec(commandArg);
117
+ this.assertSucceeded(commandArg, result);
118
+ return result;
119
+ }
120
+
121
+ private assertSucceeded(
122
+ commandArg: string,
123
+ resultArg: plugins.smartshell.IExecResult,
124
+ causeArg?: unknown,
125
+ ): void {
126
+ if (resultArg.exitCode === 0 && !resultArg.signal) {
127
+ return;
93
128
  }
129
+ throw new SmartDaemonCommandError(
130
+ commandArg,
131
+ resultArg.exitCode,
132
+ resultArg.signal,
133
+ resultArg.stderr,
134
+ causeArg === undefined ? undefined : { cause: causeArg },
135
+ );
94
136
  }
95
137
 
96
138
  /**
@@ -136,7 +178,8 @@ export class SmartDaemonSystemdManager {
136
178
  if (await this.checkElegibility()) {
137
179
  const content = this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg);
138
180
  const targetPath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
139
-
181
+
182
+ await this.ensureRootStatus();
140
183
  if (this.isRoot) {
141
184
  // Direct write when running as root
142
185
  await this.smartFs.file(targetPath).encoding('utf8').mode(0o644).write(content);
@@ -153,7 +196,8 @@ export class SmartDaemonSystemdManager {
153
196
  public async deleteService(serviceArg: SmartDaemonService) {
154
197
  if (await this.checkElegibility()) {
155
198
  const filePath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
156
-
199
+
200
+ await this.ensureRootStatus();
157
201
  if (this.isRoot) {
158
202
  await this.smartFs.file(filePath).delete();
159
203
  } else {
@@ -6,15 +6,14 @@ import { constants as fsConstants } from 'node:fs';
6
6
  import * as crypto from 'node:crypto';
7
7
  import * as util from 'node:util';
8
8
  import * as os from 'node:os';
9
+ import * as timers from 'node:timers/promises';
9
10
  export type { Stats } from 'node:fs';
10
11
 
11
- export { path, childProcess, fs, fsConstants, crypto, util, os };
12
+ export { path, childProcess, fs, fsConstants, crypto, util, os, timers };
12
13
 
13
14
  // @pushrocks scope
14
15
  import * as lik from '@push.rocks/lik';
15
16
  import * as smartfs from '@push.rocks/smartfs';
16
- import * as smartlog from '@push.rocks/smartlog';
17
- import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
18
17
  import * as smartmarkdown from '@push.rocks/smartmarkdown/iso';
19
18
  import * as smartpath from '@push.rocks/smartpath';
20
19
  import * as smartshell from '@push.rocks/smartshell';
@@ -22,8 +21,6 @@ import * as smartshell from '@push.rocks/smartshell';
22
21
  export {
23
22
  lik,
24
23
  smartfs,
25
- smartlog,
26
- smartlogDestinationLocal,
27
24
  smartmarkdown,
28
25
  smartpath,
29
26
  smartshell,
@@ -19,6 +19,9 @@ export const outputAppendMajor = 240;
19
19
  /** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
20
20
  export const fileDescriptorStoreCleanMajor = 254;
21
21
 
22
+ /** Internal: the systemd major whose `systemctl kill` first takes `--kill-whom=` (formerly `--kill-who=`). */
23
+ export const killWhomMajor = 252;
24
+
22
25
  /**
23
26
  * Internal: the systemd major whose `systemd-run` first takes `--expand-environment` and reports a
24
27
  * started scope as `runInTransientScope()` reads it.
@@ -1,2 +0,0 @@
1
- import * as plugins from './smartdaemon.plugins.js';
2
- export declare const logger: plugins.smartlog.Smartlog;
@@ -1,14 +0,0 @@
1
- import * as plugins from './smartdaemon.plugins.js';
2
- export const logger = new plugins.smartlog.Smartlog({
3
- logContext: {
4
- company: 'Some Company',
5
- companyunit: 'Some CompanyUnit',
6
- containerName: 'Some Containername',
7
- environment: 'local',
8
- runtime: 'node',
9
- zone: 'gitzone',
10
- },
11
- minimumLogLevel: 'silly',
12
- });
13
- logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal());
14
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYW1vbi5sb2dnaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc21hcnRkYW1vbi5sb2dnaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sMEJBQTBCLENBQUM7QUFFcEQsTUFBTSxDQUFDLE1BQU0sTUFBTSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUM7SUFDbEQsVUFBVSxFQUFFO1FBQ1YsT0FBTyxFQUFFLGNBQWM7UUFDdkIsV0FBVyxFQUFFLGtCQUFrQjtRQUMvQixhQUFhLEVBQUUsb0JBQW9CO1FBQ25DLFdBQVcsRUFBRSxPQUFPO1FBQ3BCLE9BQU8sRUFBRSxNQUFNO1FBQ2YsSUFBSSxFQUFFLFNBQVM7S0FDaEI7SUFDRCxlQUFlLEVBQUUsT0FBTztDQUN6QixDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsaUJBQWlCLENBQUMsSUFBSSxPQUFPLENBQUMsd0JBQXdCLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDIn0=
@@ -1,15 +0,0 @@
1
- import * as plugins from './smartdaemon.plugins.js';
2
-
3
- export const logger = new plugins.smartlog.Smartlog({
4
- logContext: {
5
- company: 'Some Company',
6
- companyunit: 'Some CompanyUnit',
7
- containerName: 'Some Containername',
8
- environment: 'local',
9
- runtime: 'node',
10
- zone: 'gitzone',
11
- },
12
- minimumLogLevel: 'silly',
13
- });
14
-
15
- logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal());