initial upload
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Makefile for DaemonTamer JPM build
|
||||
|
||||
JC := jpm build --build-type=release --ldflags="-s"
|
||||
|
||||
all: clean
|
||||
$(JC)
|
||||
|
||||
static: clean
|
||||
$(JC) --janet-cflags="-static"
|
||||
|
||||
clean:
|
||||
jpm clean
|
||||
|
||||
install:
|
||||
jpm install
|
||||
@@ -0,0 +1,93 @@
|
||||
# DaemonTamer v3: a simple way to daemonize any user command
|
||||
|
||||
This is the third iteration of the DaemonTamer project (hence DT), a simple and straightforward user-side daemon manager. DT's only purpose is allow the users to turn any shell command into a background-running
|
||||
process by registering it in a special configuration file, and to manage it with a standard set of subcommands.
|
||||
|
||||
Previous DT iterations were written in pure POSIX shell and experimentally rewritten in Python. The current version is fully hand-written from scratch in [Janet](https://janet-lang.org).
|
||||
|
||||
## Functionality
|
||||
|
||||
DT does:
|
||||
- allow you to register any shell command to run in background,
|
||||
- change the working directory according to the service configuration,
|
||||
- run pre-start or post-stop hooks if they were specified,
|
||||
- prevent the services from double-starting,
|
||||
- allow you to specify custom paths for stdout and stderr logs,
|
||||
- persist its service configuration in a JSON file (`$HOME/.dt.json`),
|
||||
- provide you with the current status of any registered service or all at once.
|
||||
|
||||
DT does **not**:
|
||||
- supervise processes and/or apply any restart policy,
|
||||
- elevate or drop privileges,
|
||||
- provide any "autostart" capability upon the OS boot (it's up to the users to append corresponding `dt start` commands to their `.profile` or other autostart files).
|
||||
|
||||
## Building and installation
|
||||
|
||||
Besides Git and Make, you need to jave Janet, JPM and Spork installed as the dependencies. Refer to their official documentation and the documentation of your distro.
|
||||
|
||||
Once ready, just run `make` or `make static` to build and then `sudo make install` (or `sudo jpm install`) to install.
|
||||
|
||||
## Installation as a script
|
||||
|
||||
Alternatively, if you have Janet and Spork installed but don't want to compile the binary, you can use DT as a script like this (feel free to use any other directory in your `$PATH`):
|
||||
|
||||
```
|
||||
sudo cp dt.janet /usr/local/bin/dt
|
||||
sudo chmod +x /usr/local/bin/dt
|
||||
```
|
||||
|
||||
## Ready-made builds
|
||||
|
||||
Static DT binary builds, if any, are going to be provided in the "Releases" section for Linux/x86_64 and Linux/ARM64 platforms only.
|
||||
|
||||
## Usage
|
||||
|
||||
### Registering a command as a service
|
||||
|
||||
To register a command `command` under the service name `servicename`, run the following:
|
||||
|
||||
```
|
||||
dt add servicename -c 'command' [-d workdir] [-o stdout-log-file] [-e stderr-log-file] [-b 'before-start-cmd'] [-a 'after-stop-cmd']
|
||||
```
|
||||
|
||||
The parameters are:
|
||||
|
||||
```
|
||||
-a, --after-stop VALUE Post-stop hook command
|
||||
-b, --before-start VALUE Pre-start hook command
|
||||
-c, --command VALUE The command to run
|
||||
-d, --directory VALUE Working directory
|
||||
-e, --stderr-log VALUE=/dev/null Log file for stderr (default /dev/null)
|
||||
-o, --stdout-log VALUE=/dev/null Log file for stdout (default /dev/null)
|
||||
```
|
||||
|
||||
Note that you can overwrite/update an existing service entry by providing the same service name, so be careful.
|
||||
|
||||
### Unregistering (deleting) a service
|
||||
|
||||
Run `dt del servicename` and confirm your choice by entering `y`.
|
||||
|
||||
### Getting a service status
|
||||
|
||||
Run `dt status servicename` and see whether the service is running or stopped.
|
||||
|
||||
### Listing all services with their statuses
|
||||
|
||||
Run `dt list`. The columns will contain: service name, status, command, working directory.
|
||||
|
||||
### Starting a service
|
||||
|
||||
Run `dt start servicename`. If available, a before-start hook command will run first.
|
||||
|
||||
### Stopping a service
|
||||
|
||||
Run `dt stop servicename`. If available, an after-stop hook command will run after stopping.
|
||||
|
||||
### Restarting a service
|
||||
|
||||
Run `dt restart servicename`. Identical to `dt stop servicename && dt start servicename`.
|
||||
|
||||
## Credits
|
||||
|
||||
Created by Luxferre in 2026, released into the public domain with no warranties.
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env janet
|
||||
# DaemonTamer v3: a simple and portable way to daemonize any command,
|
||||
# now ported to Janet (depends on spork)
|
||||
# See README.md for (optional) compilation and usage
|
||||
# Created by Luxferre in 2026, released into the public domain
|
||||
|
||||
(import spork/json)
|
||||
(import spork/path)
|
||||
(import spork/sh)
|
||||
(import spork/argparse :prefix "")
|
||||
|
||||
(def script-path (-> (dyn :current-file) os/realpath path/dirname))
|
||||
(def config-path (string/format "%s/.dt.json" (os/getenv "HOME")))
|
||||
(def tempdir (sh/exec-slurp "/bin/sh" "-c" "dirname $(mktemp -u)"))
|
||||
(def pid-file-template (string/format "%s/dt.%%s.pid" tempdir))
|
||||
(def nohup-template "nohup %s >>%s 2>>%s &\necho $! > %s")
|
||||
|
||||
(defn- read-config []
|
||||
(if (os/stat config-path) (json/decode (slurp config-path)) @{}))
|
||||
|
||||
(defn- write-config [cfg]
|
||||
(spit config-path (json/encode cfg)))
|
||||
|
||||
(defn- to-service-entry [params]
|
||||
{
|
||||
:command (get params "command")
|
||||
:directory (or (get params "directory") script-path)
|
||||
:before-start (get params "before-start")
|
||||
:after-stop (get params "after-stop")
|
||||
:stdout-log (or (get params "stdout-log") "/dev/null")
|
||||
:stderr-log (or (get params "stderr-log") "/dev/null")
|
||||
})
|
||||
|
||||
(defn- pid-file-path [sname]
|
||||
(string/format pid-file-template sname))
|
||||
|
||||
(defn process-exists? [pid]
|
||||
(default pid (os/getpid))
|
||||
(= 0 (os/execute ["kill" "-0" (string pid)] :p)))
|
||||
|
||||
# Read a single-letter choice from the stdin
|
||||
(defn get-choice [prompt]
|
||||
(let [rawc (getline prompt)]
|
||||
(if (> (length rawc) 0)
|
||||
(string/ascii-lower (string/slice rawc 0 1))
|
||||
nil)))
|
||||
|
||||
# Ask user for y/n confirmation (with custom prompt)
|
||||
(defn user-confirm [prompt]
|
||||
(var resp nil)
|
||||
(var valid false)
|
||||
(while (not valid)
|
||||
(set resp (get-choice prompt))
|
||||
(set valid (string/check-set "yn" resp)))
|
||||
(= resp "y"))
|
||||
|
||||
# command handlers
|
||||
|
||||
(defn- service-status [sname]
|
||||
(let [pfile (pid-file-path sname)]
|
||||
(if (os/stat pfile) (let [pid (scan-number (string/trim (slurp pfile)))]
|
||||
(if (process-exists? pid) "running" "stopped"))
|
||||
"stopped")))
|
||||
|
||||
(defn- start-service [sname entry]
|
||||
(if (= (service-status sname) "stopped") (do
|
||||
(printf "Starting service %s..." sname)
|
||||
(let [
|
||||
workdir (entry :directory)
|
||||
pre-start (get entry :before-start)
|
||||
cmd (get entry :command)
|
||||
sout (get entry :stdout-log)
|
||||
serr (get entry :stderr-log)
|
||||
]
|
||||
(os/cd workdir)
|
||||
(when pre-start (os/shell pre-start))
|
||||
(os/shell (string/format nohup-template cmd sout serr (pid-file-path sname)))
|
||||
(printf "Service %s started" sname)))
|
||||
# already running service here
|
||||
(eprintf "Service %s is already running!" sname)))
|
||||
|
||||
(defn- stop-service [sname entry]
|
||||
(printf "Stopping service %s..." sname)
|
||||
(let [
|
||||
pfile (pid-file-path sname)
|
||||
post-stop (get entry :after-stop)
|
||||
]
|
||||
(when (os/stat pfile) (let [pid (scan-number (string/trim (slurp pfile)))]
|
||||
(os/execute ["kill" (string pid)] :p)
|
||||
(os/rm pfile)
|
||||
(when post-stop (os/shell post-stop))
|
||||
(printf "Service %s stopped" sname)
|
||||
))))
|
||||
|
||||
(defn- restart-service [sname entry]
|
||||
(printf "Restarting service %s..." sname)
|
||||
(stop-service sname entry)
|
||||
(start-service sname entry)
|
||||
)
|
||||
|
||||
(defn process-action [action service-name params]
|
||||
(let [cfg (read-config)]
|
||||
(case action
|
||||
"add" (let [service-entry (to-service-entry params)]
|
||||
(put cfg service-name service-entry)
|
||||
(write-config cfg)
|
||||
(printf "Service %s registered" service-name)
|
||||
)
|
||||
"del" (let [service-entry (get cfg service-name)]
|
||||
(if service-entry
|
||||
(when (user-confirm (string/format "Really delete the service %s? (y/n) " service-name))
|
||||
(put cfg service-name nil)
|
||||
(write-config cfg)
|
||||
(printf "Service %s unregistered" service-name))
|
||||
(eprint "This service name does not exist!")))
|
||||
"list" (eachp [sname entry] cfg
|
||||
(printf "%s\t%s\t%s\t%s" sname
|
||||
(service-status sname)
|
||||
(entry "command") (entry "directory")))
|
||||
"status" (let [service-entry (get cfg service-name)]
|
||||
(if service-entry
|
||||
(printf "Service %s is %s." service-name (service-status service-name))
|
||||
(eprint "This service name does not exist!")))
|
||||
"start" (let [service-entry (to-service-entry (get cfg service-name))]
|
||||
(if service-entry
|
||||
(start-service service-name service-entry)
|
||||
(eprint "This service name does not exist!")))
|
||||
"stop" (let [service-entry (to-service-entry (get cfg service-name))]
|
||||
(if service-entry
|
||||
(stop-service service-name service-entry)
|
||||
(eprint "This service name does not exist!")))
|
||||
"restart" (let [service-entry (to-service-entry (get cfg service-name))]
|
||||
(if service-entry
|
||||
(restart-service service-name service-entry)
|
||||
(eprint "This service name does not exist!")))
|
||||
(eprint "Invalid action! Allowed actions: add, del, start, stop, restart, list, status"))))
|
||||
|
||||
(def cli-params
|
||||
["DaemonTamer v3 service manager (by Luxferre, 2026)"
|
||||
:default {:kind :accumulate :required true :help "[action] [name]"} # positional params: action, name
|
||||
"command" {:kind :option :short "c" :help "The command to run"}
|
||||
"directory" {:kind :option :short "d" :help "Working directory"}
|
||||
"stdout-log" {:kind :option :short "o" :help "Log file for stdout (default /dev/null)" :default "/dev/null"}
|
||||
"stderr-log" {:kind :option :short "e" :help "Log file for stderr (default /dev/null)" :default "/dev/null"}
|
||||
"before-start" {:kind :option :short "b" :help "Pre-start hook command"}
|
||||
"after-stop" {:kind :option :short "a" :help "Post-stop hook command"}])
|
||||
|
||||
(defn main [&]
|
||||
(let [args (argparse ;cli-params)]
|
||||
(def posargs (get args :default))
|
||||
(if (and posargs (or (>= (length posargs) 2) (= (posargs 0) "list")))
|
||||
(do
|
||||
(def [action service-name] (args :default))
|
||||
(process-action action service-name args)
|
||||
) (do
|
||||
(eprint "Usage: dt {action} {service-name} [opts]\nAllowed actions: add, del, start, stop, restart, list, status")
|
||||
(os/exit 1)
|
||||
))))
|
||||
@@ -0,0 +1,11 @@
|
||||
(declare-project
|
||||
:name "DaemonTamer"
|
||||
:description "A simple tool to daemonize any user command")
|
||||
|
||||
(declare-source
|
||||
:source ["dt.janet"])
|
||||
|
||||
(declare-executable
|
||||
:name "dt"
|
||||
:entry "dt.janet"
|
||||
:install true)
|
||||
Reference in New Issue
Block a user