initial upload

This commit is contained in:
Luxferre
2026-04-04 13:11:12 +03:00
commit 49d17a09aa
4 changed files with 277 additions and 0 deletions
+15
View File
@@ -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
+93
View File
@@ -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.
Executable
+158
View File
@@ -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)
))))
+11
View File
@@ -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)