Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
a8b6ecb1a6 | |||
972688b8be | |||
0ae3b9a5ec | |||
305bb6795c | |||
ea35307c68 | |||
6af0b5d0d8 | |||
db20fb6bce | |||
3c2d5996d2 | |||
1b78b79bd8 | |||
229929f7f1 | |||
923b423536 | |||
b464b351fb | |||
31ddd3ec23 | |||
af90530000 | |||
f27fb675e5 | |||
fbfc1dd65e | |||
d034cd3725 | |||
3a2d174c19 | |||
323648a3f7 | |||
0a48d9a12d | |||
7bf11122f8 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
128
.gitlab-ci.yml
128
.gitlab-ci.yml
@@ -1,128 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
before_script:
|
||||
- pnpm install -g pnpm
|
||||
- pnpm install -g @shipzone/npmci
|
||||
- npmci npm prepare
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --prod
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --dev
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run buildDocs
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
52
changelog.md
Normal file
52
changelog.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-09-03 - 2.1.0 - feat(systemd-manager)
|
||||
Support sudo password and root detection in SystemdManager; add user/group support in services and templates; add tests and expand README
|
||||
|
||||
- SystemdManager now accepts an optional sudoPassword and detects root status on startup
|
||||
- Commands executed via SystemdManager are automatically prefixed with sudo when not running as root; supports interactive PTY flow to provide sudo password
|
||||
- saveService/deleteService now use direct writes when running as root and fallback to sudo-based moves/removals when not root
|
||||
- Service model extended with optional user and group properties
|
||||
- Template manager updated to include user/group comments and to emit User/Group directives in generated systemd unit files when provided
|
||||
- Exports updated (ts/index.ts) to include SmartDaemonService
|
||||
- New/updated tests added to verify root detection, service creation, unit file generation and sudoPassword option
|
||||
- README expanded with detailed usage examples, API docs and troubleshooting/security guidance
|
||||
|
||||
## 2025-08-29 - 2.0.9 - fix(tests)
|
||||
update test runner and developer dependencies; add pnpm workspace and packageManager
|
||||
|
||||
- Change test import to use @git.zone/tstest/tapbundle and update test script to run with --verbose, --logfile and --timeout 60
|
||||
- Bump dev dependency @git.zone/tsbuild and @git.zone/tstest to more recent releases
|
||||
- Update several @push.rocks runtime dependencies to newer patch/minor versions (lik, smartfile, smartlog, smartpath, smartshell, smartsystem)
|
||||
- Add packageManager field with pinned pnpm version
|
||||
- Add pnpm-workspace.yaml with onlyBuiltDependencies entries
|
||||
|
||||
## 2025-03-03 - 2.0.8 - fix(core)
|
||||
Update project dependencies and resolve minor issues in file paths
|
||||
|
||||
- Updated devDependencies and dependencies to latest versions
|
||||
- Resolved minor issues in file paths within systemd manager
|
||||
|
||||
## 2025-03-03 - 2.0.7 - fix(core)
|
||||
Update @types/node to fix compatibility issues.
|
||||
|
||||
- Updated @types/node from version 20.12.7 to 22.13.8 to ensure compatibility with the latest Node.js features.
|
||||
- Improved type definitions which might resolve minor type errors in development.
|
||||
|
||||
## 2024-05-29 - 2.0.6 - Documentation
|
||||
updated project description
|
||||
|
||||
## 2024-04-19 - 2.0.5 to 2.0.4 - Core Updates
|
||||
Improved core functionality across multiple versions.
|
||||
|
||||
- Updated tsconfig for better build performance.
|
||||
- Revised npmextra.json to update githost configuration.
|
||||
|
||||
## 2023-07-25 - 2.0.3 - Organizational Changes
|
||||
implemented a new organizational scheme
|
||||
|
||||
## 2022-10-19 - 2.0.0 - Core Updates and Breaking Changes
|
||||
Major overhaul with breaking changes and core updates.
|
||||
|
||||
- **Breaking Change:** Transitioned the project architecture from CommonJS to ECMAScript modules (ESM) to enhance compatibility and future-proofing.
|
||||
|
@@ -2,17 +2,29 @@
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "pushrocks",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartdaemon",
|
||||
"description": "start scripts as long running daemons and manage them",
|
||||
"npmPackagename": "@pushrocks/smartdaemon",
|
||||
"description": "Start scripts as long running daemons and manage them.",
|
||||
"npmPackagename": "@push.rocks/smartdaemon",
|
||||
"license": "MIT",
|
||||
"projectDomain": "push.rocks"
|
||||
"projectDomain": "push.rocks",
|
||||
"keywords": [
|
||||
"daemon",
|
||||
"service management",
|
||||
"Linux systemd",
|
||||
"process management",
|
||||
"Node.js",
|
||||
"system administration",
|
||||
"background services"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
50
package.json
50
package.json
@@ -1,33 +1,32 @@
|
||||
{
|
||||
"name": "@pushrocks/smartdaemon",
|
||||
"version": "2.0.3",
|
||||
"name": "@push.rocks/smartdaemon",
|
||||
"version": "2.1.0",
|
||||
"private": false,
|
||||
"description": "start scripts as long running daemons and manage them",
|
||||
"description": "Start scripts as long running daemons and manage them.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.25",
|
||||
"@gitzone/tsrun": "^1.2.38",
|
||||
"@gitzone/tstest": "^1.0.52",
|
||||
"@pushrocks/tapbundle": "^5.0.4",
|
||||
"@types/node": "^18.11.2"
|
||||
"@git.zone/tsbuild": "^2.6.7",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^2.3.5",
|
||||
"@types/node": "^22.13.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pushrocks/lik": "^6.0.0",
|
||||
"@pushrocks/smartfile": "^10.0.5",
|
||||
"@pushrocks/smartfm": "^2.0.4",
|
||||
"@pushrocks/smartlog": "^3.0.1",
|
||||
"@pushrocks/smartlog-destination-local": "^8.0.8",
|
||||
"@pushrocks/smartpath": "^5.0.5",
|
||||
"@pushrocks/smartshell": "^2.0.26",
|
||||
"@pushrocks/smartsystem": "^3.0.1"
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartfile": "^11.2.7",
|
||||
"@push.rocks/smartfm": "^2.0.4",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartlog-destination-local": "^9.0.1",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartshell": "^3.3.0",
|
||||
"@push.rocks/smartsystem": "^3.0.7"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -44,5 +43,20 @@
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"daemon",
|
||||
"service management",
|
||||
"Linux systemd",
|
||||
"process management",
|
||||
"Node.js",
|
||||
"system administration",
|
||||
"background services"
|
||||
],
|
||||
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartdaemon.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
|
||||
}
|
||||
|
11528
pnpm-lock.yaml
generated
11528
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- mongodb-memory-server
|
||||
- puppeteer
|
1
readme.hints.md
Normal file
1
readme.hints.md
Normal file
@@ -0,0 +1 @@
|
||||
|
446
readme.md
446
readme.md
@@ -1,41 +1,427 @@
|
||||
# @pushrocks/smartdaemon
|
||||
start scripts as long running daemons and manage them
|
||||
# @push.rocks/smartdaemon 🚀
|
||||
**Turn your Node.js scripts into production-ready system daemons**
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartdaemon)
|
||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/smartdaemon)
|
||||
* [github.com (source mirror)](https://github.com/pushrocks/smartdaemon)
|
||||
* [docs (typedoc)](https://pushrocks.gitlab.io/smartdaemon/)
|
||||
[](https://www.npmjs.com/package/@push.rocks/smartdaemon)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Status for master
|
||||
> Seamlessly manage long-running processes, background services, and system daemons with smart root detection, automatic privilege escalation, and systemd integration.
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
||||
## 🎯 What is SmartDaemon?
|
||||
|
||||
## Usage
|
||||
`@push.rocks/smartdaemon` is your Swiss Army knife for turning Node.js applications into bulletproof system services. Whether you're deploying microservices, running scheduled tasks, or managing background workers, SmartDaemon handles the complex stuff so you can focus on your business logic.
|
||||
|
||||
## Contribution
|
||||
### ✨ Key Features
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
- 🔐 **Smart Privilege Management**: Automatically detects root status and handles sudo operations seamlessly
|
||||
- 👤 **User/Group Control**: Run services under specific users for enhanced security
|
||||
- 🔄 **Systemd Integration**: Full Linux systemd support for production-grade service management
|
||||
- 🛡️ **Auto-Recovery**: Built-in service restart on failure
|
||||
- 📝 **Declarative Service Definitions**: Simple, clear service configuration
|
||||
- 🔌 **Interactive & Non-Interactive Modes**: Support for both passwordless sudo and interactive authentication
|
||||
- 🎛️ **Complete Lifecycle Management**: Start, stop, enable, disable, and reload services with ease
|
||||
|
||||
## Contribution
|
||||
## 📦 Installation
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @push.rocks/smartdaemon --save
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
# Using pnpm (recommended)
|
||||
pnpm add @push.rocks/smartdaemon
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
# Using yarn
|
||||
yarn add @push.rocks/smartdaemon
|
||||
```
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { SmartDaemon } from '@push.rocks/smartdaemon';
|
||||
|
||||
// Initialize SmartDaemon
|
||||
const daemon = new SmartDaemon();
|
||||
|
||||
// Create and enable a service
|
||||
const myService = await daemon.addService({
|
||||
name: 'my-api-server',
|
||||
description: 'My awesome API server',
|
||||
command: 'node server.js',
|
||||
workingDir: '/opt/myapp',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// Enable and start the service
|
||||
await myService.enable(); // Creates systemd service file
|
||||
await myService.start(); // Starts the service
|
||||
```
|
||||
|
||||
### Running Services as Specific Users 👥
|
||||
|
||||
Security best practice: Never run services as root unless absolutely necessary!
|
||||
|
||||
```typescript
|
||||
const webService = await daemon.addService({
|
||||
name: 'web-server',
|
||||
description: 'Production web server',
|
||||
command: 'node app.js',
|
||||
workingDir: '/var/www/app',
|
||||
version: '2.1.0',
|
||||
user: 'www-data', // Run as www-data user
|
||||
group: 'www-data' // Run under www-data group
|
||||
});
|
||||
|
||||
await webService.enable();
|
||||
await webService.start();
|
||||
```
|
||||
|
||||
## 🔐 Smart Privilege Escalation
|
||||
|
||||
SmartDaemon intelligently handles privilege escalation based on your execution context:
|
||||
|
||||
### Automatic Root Detection
|
||||
|
||||
```typescript
|
||||
// SmartDaemon automatically detects if running as root
|
||||
const daemon = new SmartDaemon();
|
||||
|
||||
// If running as regular user, sudo will be used automatically
|
||||
// If running as root, commands execute directly
|
||||
```
|
||||
|
||||
### With Sudo Password
|
||||
|
||||
If you're not running as root and don't have passwordless sudo configured:
|
||||
|
||||
```typescript
|
||||
const daemon = new SmartDaemon({
|
||||
sudoPassword: 'your-sudo-password' // Will be used for privilege escalation
|
||||
});
|
||||
|
||||
// All privileged operations will now use the provided password
|
||||
const service = await daemon.addService({
|
||||
name: 'privileged-service',
|
||||
description: 'Service requiring root privileges',
|
||||
command: 'node admin-task.js',
|
||||
workingDir: '/opt/admin',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
await service.enable(); // Automatically uses sudo with password
|
||||
```
|
||||
|
||||
### Passwordless Sudo (Recommended for Production)
|
||||
|
||||
For production environments, configure passwordless sudo for specific systemctl commands:
|
||||
|
||||
1. Create a sudoers file: `/etc/sudoers.d/smartdaemon`
|
||||
2. Add the following content:
|
||||
```bash
|
||||
# Allow smartdaemon to manage services without password
|
||||
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl start smartdaemon_*
|
||||
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop smartdaemon_*
|
||||
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable smartdaemon_*
|
||||
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable smartdaemon_*
|
||||
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||
```
|
||||
|
||||
## 📚 Complete API
|
||||
|
||||
### SmartDaemon Class
|
||||
|
||||
```typescript
|
||||
interface ISmartDaemonOptions {
|
||||
sudoPassword?: string; // Optional sudo password for privilege escalation
|
||||
}
|
||||
|
||||
class SmartDaemon {
|
||||
constructor(options?: ISmartDaemonOptions);
|
||||
|
||||
// Add a new service or update existing one
|
||||
async addService(options: ISmartDaemonServiceOptions): Promise<SmartDaemonService>;
|
||||
|
||||
// Direct access to managers (advanced usage)
|
||||
systemdManager: SmartDaemonSystemdManager;
|
||||
templateManager: SmartDaemonTemplateManager;
|
||||
}
|
||||
```
|
||||
|
||||
### SmartDaemonService Class
|
||||
|
||||
```typescript
|
||||
interface ISmartDaemonServiceOptions {
|
||||
name: string; // Unique service identifier
|
||||
description: string; // Human-readable description
|
||||
command: string; // Command to execute
|
||||
workingDir: string; // Working directory (absolute path)
|
||||
version: string; // Service version
|
||||
user?: string; // User to run service as (optional)
|
||||
group?: string; // Group to run service as (optional)
|
||||
}
|
||||
|
||||
class SmartDaemonService {
|
||||
// Lifecycle management
|
||||
async enable(): Promise<void>; // Install and enable service
|
||||
async disable(): Promise<void>; // Disable service
|
||||
async start(): Promise<void>; // Start service
|
||||
async stop(): Promise<void>; // Stop service
|
||||
|
||||
// Service management
|
||||
async save(): Promise<void>; // Save service configuration
|
||||
async delete(): Promise<void>; // Remove service
|
||||
async reload(): Promise<void>; // Reload systemd daemon
|
||||
}
|
||||
```
|
||||
|
||||
## 🎭 Real-World Examples
|
||||
|
||||
### Microservice Deployment
|
||||
|
||||
```typescript
|
||||
import { SmartDaemon } from '@push.rocks/smartdaemon';
|
||||
|
||||
async function deployMicroservices() {
|
||||
const daemon = new SmartDaemon();
|
||||
|
||||
// API Gateway
|
||||
const apiGateway = await daemon.addService({
|
||||
name: 'api-gateway',
|
||||
description: 'API Gateway Service',
|
||||
command: 'node --max-old-space-size=2048 gateway.js',
|
||||
workingDir: '/opt/services/gateway',
|
||||
version: '3.2.1',
|
||||
user: 'apiuser',
|
||||
group: 'apigroup'
|
||||
});
|
||||
|
||||
// Auth Service
|
||||
const authService = await daemon.addService({
|
||||
name: 'auth-service',
|
||||
description: 'Authentication Service',
|
||||
command: 'node auth-service.js',
|
||||
workingDir: '/opt/services/auth',
|
||||
version: '2.1.0',
|
||||
user: 'authuser',
|
||||
group: 'authgroup'
|
||||
});
|
||||
|
||||
// Database Sync Worker
|
||||
const dbWorker = await daemon.addService({
|
||||
name: 'db-sync-worker',
|
||||
description: 'Database Synchronization Worker',
|
||||
command: 'node workers/db-sync.js',
|
||||
workingDir: '/opt/services/workers',
|
||||
version: '1.5.3',
|
||||
user: 'dbworker',
|
||||
group: 'dbgroup'
|
||||
});
|
||||
|
||||
// Enable and start all services
|
||||
const services = [apiGateway, authService, dbWorker];
|
||||
|
||||
for (const service of services) {
|
||||
await service.enable();
|
||||
await service.start();
|
||||
console.log(`✅ Service ${service.name} is running`);
|
||||
}
|
||||
}
|
||||
|
||||
deployMicroservices();
|
||||
```
|
||||
|
||||
### Scheduled Task Runner
|
||||
|
||||
```typescript
|
||||
async function setupScheduledTasks() {
|
||||
const daemon = new SmartDaemon();
|
||||
|
||||
// Backup service that runs every night
|
||||
const backupService = await daemon.addService({
|
||||
name: 'nightly-backup',
|
||||
description: 'Nightly database backup service',
|
||||
command: 'node --require dotenv/config backup-runner.js',
|
||||
workingDir: '/opt/backup',
|
||||
version: '1.0.0',
|
||||
user: 'backup',
|
||||
group: 'backup'
|
||||
});
|
||||
|
||||
await backupService.enable();
|
||||
await backupService.start();
|
||||
}
|
||||
```
|
||||
|
||||
### Development vs Production Setup
|
||||
|
||||
```typescript
|
||||
import { SmartDaemon } from '@push.rocks/smartdaemon';
|
||||
|
||||
async function setupService(environment: 'development' | 'production') {
|
||||
let daemonOptions = {};
|
||||
|
||||
if (environment === 'development') {
|
||||
// In development, you might use sudo password
|
||||
daemonOptions = {
|
||||
sudoPassword: process.env.SUDO_PASSWORD
|
||||
};
|
||||
}
|
||||
// In production, rely on passwordless sudo configuration
|
||||
|
||||
const daemon = new SmartDaemon(daemonOptions);
|
||||
|
||||
const service = await daemon.addService({
|
||||
name: `app-${environment}`,
|
||||
description: `Application ${environment} server`,
|
||||
command: environment === 'production'
|
||||
? 'node --production app.js'
|
||||
: 'node --inspect app.js',
|
||||
workingDir: '/opt/application',
|
||||
version: '1.0.0',
|
||||
user: environment === 'production' ? 'appuser' : process.env.USER,
|
||||
group: environment === 'production' ? 'appgroup' : process.env.USER
|
||||
});
|
||||
|
||||
await service.enable();
|
||||
await service.start();
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Advanced Usage
|
||||
|
||||
### Direct SystemdManager Access
|
||||
|
||||
For advanced users who need fine-grained control:
|
||||
|
||||
```typescript
|
||||
const daemon = new SmartDaemon();
|
||||
|
||||
// Access the SystemdManager directly
|
||||
const systemdManager = daemon.systemdManager;
|
||||
|
||||
// Check if running on a compatible system
|
||||
const canRun = await systemdManager.checkElegibility();
|
||||
|
||||
// Get all existing SmartDaemon services
|
||||
const existingServices = await systemdManager.getServices();
|
||||
|
||||
// Reload systemd daemon
|
||||
await systemdManager.reload();
|
||||
```
|
||||
|
||||
### Custom Service Templates
|
||||
|
||||
Access the template manager for custom service file generation:
|
||||
|
||||
```typescript
|
||||
const daemon = new SmartDaemon();
|
||||
const template = daemon.templateManager.generateUnitFileForService(myService);
|
||||
console.log(template); // View generated systemd unit file
|
||||
```
|
||||
|
||||
## 🏗️ System Requirements
|
||||
|
||||
- **Operating System**: Linux with systemd (Ubuntu 16.04+, Debian 9+, CentOS 7+, RHEL 7+, Fedora, Arch, etc.)
|
||||
- **Node.js**: Version 14.x or higher
|
||||
- **Permissions**: Either root access or properly configured sudo
|
||||
|
||||
## ⚙️ Generated Systemd Service Files
|
||||
|
||||
SmartDaemon generates professional systemd unit files with:
|
||||
- Automatic restart on failure
|
||||
- Network target dependencies
|
||||
- Proper working directory configuration
|
||||
- User/Group assignment
|
||||
- Resource limits configuration
|
||||
- Syslog integration for logging
|
||||
- Environment variable support
|
||||
|
||||
Example generated service file:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=My API Server
|
||||
Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Environment=NODE_OPTIONS="--max_old_space_size=100"
|
||||
ExecStart=/bin/bash -c "cd /opt/myapp && node server.js"
|
||||
WorkingDirectory=/opt/myapp
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=infinity
|
||||
LimitCORE=infinity
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
If you encounter "Interactive authentication required" errors:
|
||||
|
||||
1. **Option 1**: Run your application with sudo
|
||||
```bash
|
||||
sudo node your-app.js
|
||||
```
|
||||
|
||||
2. **Option 2**: Configure passwordless sudo (recommended for production)
|
||||
|
||||
3. **Option 3**: Provide sudo password programmatically
|
||||
```typescript
|
||||
const daemon = new SmartDaemon({ sudoPassword: 'your-password' });
|
||||
```
|
||||
|
||||
### Service Not Found
|
||||
|
||||
If systemctl can't find your service:
|
||||
- Check that the service was enabled: `await service.enable()`
|
||||
- Verify the service name: Services are prefixed with `smartdaemon_`
|
||||
- Reload systemd: `await service.reload()`
|
||||
|
||||
### Service Fails to Start
|
||||
|
||||
Check logs using:
|
||||
```bash
|
||||
journalctl -u smartdaemon_your-service-name -f
|
||||
```
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
1. **Never run services as root** unless absolutely necessary
|
||||
2. **Use dedicated service users** with minimal permissions
|
||||
3. **Configure passwordless sudo** for production instead of storing passwords
|
||||
4. **Limit sudo permissions** to only the specific systemctl commands needed
|
||||
5. **Regular security audits** of your service configurations
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
- 📧 Email: hello@task.vc
|
||||
- 🐛 Issues: [GitHub Issues](https://code.foss.global/push.rocks/smartdaemon/issues)
|
||||
- 📖 Documentation: [https://code.foss.global/push.rocks/smartdaemon](https://code.foss.global/push.rocks/smartdaemon)
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
@@ -1,5 +1,3 @@
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
|
||||
export {
|
||||
smartpath
|
||||
};
|
||||
export { smartpath };
|
||||
|
85
test/test.smartdaemon.ts
Normal file
85
test/test.smartdaemon.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartdaemon from '../ts/index.js';
|
||||
|
||||
let testSmartdaemon: smartdaemon.SmartDaemon;
|
||||
|
||||
tap.test('should create an instance of smartdaemon', async () => {
|
||||
testSmartdaemon = new smartdaemon.SmartDaemon();
|
||||
expect(testSmartdaemon).toBeInstanceOf(smartdaemon.SmartDaemon);
|
||||
});
|
||||
|
||||
tap.test('should detect root status correctly', async () => {
|
||||
const isRoot = await testSmartdaemon.systemdManager['checkIsRoot']();
|
||||
console.log(`Running as root: ${isRoot}`);
|
||||
expect(isRoot).toBeTypeofBoolean();
|
||||
});
|
||||
|
||||
tap.test('should create service with user/group properties', async () => {
|
||||
const testService = await smartdaemon.SmartDaemonService.createFromOptions(
|
||||
testSmartdaemon,
|
||||
{
|
||||
name: 'test-service',
|
||||
description: 'A test service',
|
||||
command: 'node test.js',
|
||||
workingDir: '/tmp',
|
||||
version: '1.0.0',
|
||||
user: 'www-data',
|
||||
group: 'www-data'
|
||||
}
|
||||
);
|
||||
|
||||
expect(testService.user).toEqual('www-data');
|
||||
expect(testService.group).toEqual('www-data');
|
||||
});
|
||||
|
||||
tap.test('should generate systemd unit file with User/Group directives', async () => {
|
||||
const testService = await smartdaemon.SmartDaemonService.createFromOptions(
|
||||
testSmartdaemon,
|
||||
{
|
||||
name: 'test-service',
|
||||
description: 'A test service',
|
||||
command: 'node test.js',
|
||||
workingDir: '/tmp',
|
||||
version: '1.0.0',
|
||||
user: 'www-data',
|
||||
group: 'www-data'
|
||||
}
|
||||
);
|
||||
|
||||
const unitFileContent = testSmartdaemon.templateManager.generateUnitFileForService(testService);
|
||||
console.log('Generated unit file:');
|
||||
console.log(unitFileContent);
|
||||
|
||||
expect(unitFileContent).toInclude('User=www-data');
|
||||
expect(unitFileContent).toInclude('Group=www-data');
|
||||
expect(unitFileContent).toInclude('# user: www-data');
|
||||
expect(unitFileContent).toInclude('# group: www-data');
|
||||
});
|
||||
|
||||
tap.test('should handle services without user/group', async () => {
|
||||
const testService = await smartdaemon.SmartDaemonService.createFromOptions(
|
||||
testSmartdaemon,
|
||||
{
|
||||
name: 'test-service-no-user',
|
||||
description: 'A test service without user',
|
||||
command: 'node test.js',
|
||||
workingDir: '/tmp',
|
||||
version: '1.0.0'
|
||||
}
|
||||
);
|
||||
|
||||
const unitFileContent = testSmartdaemon.templateManager.generateUnitFileForService(testService);
|
||||
|
||||
expect(unitFileContent).not.toInclude('User=');
|
||||
expect(unitFileContent).not.toInclude('Group=');
|
||||
});
|
||||
|
||||
tap.test('should create smartdaemon with sudo password option', async () => {
|
||||
const daemonWithPassword = new smartdaemon.SmartDaemon({
|
||||
sudoPassword: 'test-password'
|
||||
});
|
||||
|
||||
expect(daemonWithPassword.systemdManager.sudoPassword).toEqual('test-password');
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -1,11 +1,9 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import * as smartdaemon from '../ts/index.js';
|
||||
|
||||
|
||||
|
||||
let testSmartdaemon: smartdaemon.SmartDaemon;
|
||||
|
||||
tap.test('should create an instance of smartdaemon', async () => {
|
||||
@@ -23,4 +21,4 @@ tap.test('should create a service', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
tap.start();
|
||||
export default tap.start();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@pushrocks/smartdaemon',
|
||||
version: '2.0.3',
|
||||
description: 'start scripts as long running daemons and manage them'
|
||||
name: '@push.rocks/smartdaemon',
|
||||
version: '2.1.0',
|
||||
description: 'Start scripts as long running daemons and manage them.'
|
||||
}
|
||||
|
@@ -1 +1,2 @@
|
||||
export * from './smartdaemon.classes.smartdaemon.js';
|
||||
export * from './smartdaemon.classes.service.js';
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -1,18 +1,22 @@
|
||||
import * as plugins from './smartdaemon.plugins.js';
|
||||
import { SmartDaemonTemplateManager } from './smartdaemon.classes.templatemanager.js';
|
||||
import {
|
||||
type ISmartDaemonServiceConstructorOptions,
|
||||
SmartDaemonService,
|
||||
ISmartDaemonServiceConstructorOptions,
|
||||
} 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,7 +2,7 @@ import * as plugins from './smartdaemon.plugins.js';
|
||||
import * as paths from './smartdaemon.paths.js';
|
||||
import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
|
||||
import {
|
||||
ISmartDaemonServiceConstructorOptions,
|
||||
type ISmartDaemonServiceConstructorOptions,
|
||||
SmartDaemonService,
|
||||
} from './smartdaemon.classes.service.js';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import * as plugins from './smartdaemon.plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
export const systemdDir = plugins.path.join('/lib/systemd/system/');
|
||||
|
@@ -4,15 +4,24 @@ import * as path from 'path';
|
||||
export { path };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as lik from '@pushrocks/lik';
|
||||
import * as smartfile from '@pushrocks/smartfile';
|
||||
import * as smartfm from '@pushrocks/smartfm';
|
||||
import * as smartlog from '@pushrocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@pushrocks/smartlog-destination-local';
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import * as smartshell from '@pushrocks/smartshell';
|
||||
import * as smartsystem from '@pushrocks/smartsystem';
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartfm from '@push.rocks/smartfm';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
import * as smartsystem from '@push.rocks/smartsystem';
|
||||
|
||||
export { lik, smartfile, smartfm, smartlog, smartlogDestinationLocal, smartpath, smartshell, smartsystem };
|
||||
export {
|
||||
lik,
|
||||
smartfile,
|
||||
smartfm,
|
||||
smartlog,
|
||||
smartlogDestinationLocal,
|
||||
smartpath,
|
||||
smartshell,
|
||||
smartsystem,
|
||||
};
|
||||
|
||||
// third party
|
||||
|
@@ -3,8 +3,12 @@
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
17
tslint.json
17
tslint.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user