init
This commit is contained in:
commit
a836916098
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.v]
|
||||||
|
indent_style = tab
|
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.bat eol=crlf
|
||||||
|
|
||||||
|
**/*.v linguist-language=V
|
||||||
|
**/*.vv linguist-language=V
|
||||||
|
**/*.vsh linguist-language=V
|
||||||
|
**/v.mod linguist-language=V
|
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
pt
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Ignore binary output folders
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Ignore common editor/system specific metadata
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# ENV
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vweb and database
|
||||||
|
*.db
|
||||||
|
*.js
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
PT_VERSION ?= 0.0.1
|
||||||
|
|
||||||
|
all: prod
|
||||||
|
|
||||||
|
dev:
|
||||||
|
v -o pt src/
|
||||||
|
|
||||||
|
prod:
|
||||||
|
v -prod -cc gcc -cflags -static -o pt src/ \
|
||||||
|
-d pt_version=$(PT_VERSION) \
|
||||||
|
-d pt_piddir=pt \
|
||||||
|
-d pt_max_recursion_depth=10 \
|
||||||
|
-d pt_default_config_file=~/.ptrc
|
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# pt — daemonless background processes for Linux (WIP)
|
||||||
|
|
||||||
|
Run and manage background processes without daemon or root privileges. `pt` is a small process manager with limited capabilities
|
||||||
|
|
||||||
|
`pt` stands for process tool.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Run arbitrary command in background. The process will be adopted by /sbin/init.
|
||||||
|
- No daemon needed. `pt` just stores pidfile in runtime directory and checks procfs on invokation.
|
||||||
|
- Run commands defined in the configuration file.
|
||||||
|
- Set environment and working directory for process.
|
||||||
|
- Run commands selected by labels.
|
||||||
|
- Print defined commands and currently running commands.
|
||||||
|
- [not implemented] Run commands without writing configuration file.
|
||||||
|
- [not implemented] TUI.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
First install [V compiler](https://github.com/vlang/v).
|
||||||
|
|
||||||
|
Clone this repo and do:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd pt
|
||||||
|
make
|
||||||
|
install -Dm0755 pt $HOME/.local/bin/pt
|
||||||
|
install -Dm0644 completion.bash ${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/pt
|
||||||
|
```
|
||||||
|
|
||||||
|
Next step is configuration.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Default configuration file is `~/.ptrc`. This is [TOML](https://toml.io) format file.
|
||||||
|
|
||||||
|
See full configuration example with comments in [ptrc.toml](ptrc.toml).
|
37
completion.bash
Normal file
37
completion.bash
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# ex: filetype=sh
|
||||||
|
# shellcheck disable=SC2207
|
||||||
|
_pt_completions()
|
||||||
|
{
|
||||||
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
local prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
COMPREPLY=($(compgen -W '-debug -c -config -help' -- "$cur"))
|
||||||
|
case "$prev" in
|
||||||
|
-c|-config)
|
||||||
|
COMPREPLY+=($(compgen -f -- "$cur"))
|
||||||
|
;;
|
||||||
|
-l|-label)
|
||||||
|
if [[ ${COMP_WORDS[*]} =~ start ]]; then
|
||||||
|
COMPREPLY+=($(compgen -W "$(./pt labels)" -- "$cur"))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
local words=($(./pt ls -o brief) start stop signal)
|
||||||
|
if [[ ${words[*]} =~ $prev ]]; then
|
||||||
|
COMPREPLY+=($(compgen -W "$(./pt ls -o brief)" -- "$cur"))
|
||||||
|
fi
|
||||||
|
local commands='start stop ls ps help version signal labels'
|
||||||
|
local invoked=()
|
||||||
|
for comm in $commands; do
|
||||||
|
if [[ ${COMP_WORDS[*]} =~ $comm ]]; then
|
||||||
|
invoked+=("$comm")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Do not complete commands if any command is already invoked
|
||||||
|
if [[ "${#invoked[@]}" == 0 ]]; then
|
||||||
|
COMPREPLY+=($(compgen -W "$commands" -- "$cur"))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -o filenames -F _pt_completions pt
|
39
ptrc.toml
Normal file
39
ptrc.toml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Runtime directory. You shouldn't set rundir without a good reason. Omit this
|
||||||
|
# parameter if everything works fine automatically.
|
||||||
|
# rundir = '/run/user/1000'
|
||||||
|
|
||||||
|
# You can include another config files using path glob. This is suitable for
|
||||||
|
# splitting configuration into separate files. All configs will recursively
|
||||||
|
# loaded. Max recursion depth is 10. pt will warn if some files aren't loaded.
|
||||||
|
# You need to recompile pt to change max_recursion_depth.
|
||||||
|
include = '~/.config/pt.d/*.toml'
|
||||||
|
|
||||||
|
# The command entry defenition. There 'sleep' is an entry name. TOML syntax
|
||||||
|
# allows you to use quoting for non-letter names e.g. [entry."hello@world"]
|
||||||
|
# All entry parameters described below.
|
||||||
|
[entry.sleep]
|
||||||
|
# PID file. You should not to set pidfile is most cases. Depends on rundir.
|
||||||
|
# Filename pattern is `{rundir}/{piddir}/{entry.name}.pid`. pidddir is always
|
||||||
|
# 'pt'.
|
||||||
|
# pidfile = '/run/user/1000/pt/sleep.pid'
|
||||||
|
|
||||||
|
# Working directory. If the process should be executed in a specific directory
|
||||||
|
# you can specify it here. If not specified, the current working directory is
|
||||||
|
# used.
|
||||||
|
workdir = '.'
|
||||||
|
|
||||||
|
# Labels do not affect the operation of the process in any way, but can be used
|
||||||
|
# to group commands and run them with `pt start -l label1 -l label2`.
|
||||||
|
labels = ['example']
|
||||||
|
|
||||||
|
# exec is array of strings with executable and arguments. Describing a command
|
||||||
|
# using this syntax may seem awkward, but it allows you to clearly and
|
||||||
|
# unambiguously define arguments that may include spaces and other special
|
||||||
|
# characters.
|
||||||
|
exec = ['/usr/bin/sleep', '100']
|
||||||
|
|
||||||
|
# You can pass environment variables to a process by declaring them as map.
|
||||||
|
env = { PATH = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin' }
|
||||||
|
|
||||||
|
# Description. Leave a note explaining what this command is for.
|
||||||
|
description = 'Just sleep'
|
109
src/em.v
Normal file
109
src/em.v
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import arrays
|
||||||
|
import os
|
||||||
|
import log
|
||||||
|
|
||||||
|
struct EntryManager {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn EntryManager.new(config Config) EntryManager {
|
||||||
|
return EntryManager{
|
||||||
|
config: config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) labels() []string {
|
||||||
|
mut labels := []string{}
|
||||||
|
for _, val in em.config.entries {
|
||||||
|
labels << val.labels
|
||||||
|
}
|
||||||
|
return arrays.uniq(labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) by_labels(labels []string) []Entry {
|
||||||
|
log.debug('Lookup entries by labels: ${labels}')
|
||||||
|
mut entries := []Entry{}
|
||||||
|
for _, val in em.config.entries {
|
||||||
|
mut found := true
|
||||||
|
for label in labels {
|
||||||
|
if label !in val.labels {
|
||||||
|
found = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == true {
|
||||||
|
entries << val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) by_name(name string) !Entry {
|
||||||
|
log.debug('Lookup entry: ${name}')
|
||||||
|
return em.config.entries[name] or { error('No such entry named ${name}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) run(entry Entry) ! {
|
||||||
|
is_running := em.is_running(entry.name) or { false }
|
||||||
|
if is_running {
|
||||||
|
log.warn("Entry '${entry.name}' is already running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.debug('Starting up entry: ${entry.name}')
|
||||||
|
log.debug('${entry}')
|
||||||
|
mut process := os.new_process(entry.exec[0])
|
||||||
|
process.set_args(entry.exec[1..])
|
||||||
|
process.set_environment(entry.env)
|
||||||
|
process.set_work_folder(if entry.workdir == '' { os.getwd() } else { os.abs_path(entry.workdir) })
|
||||||
|
process.run()
|
||||||
|
pidfile := get_pidfile_path(rundir: em.config.rundir, entry: entry.name, path: entry.pidfile)
|
||||||
|
write_pidfile(pidfile, process.pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) signal(name string, signal os.Signal) ! {
|
||||||
|
entry := em.by_name(name) or { return err }
|
||||||
|
pidfile := get_pidfile_path(rundir: em.config.rundir, entry: entry.name, path: entry.pidfile)
|
||||||
|
pid := read_pidfile(pidfile)
|
||||||
|
send_signal(pid, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) processes() []Entry {
|
||||||
|
mut pidfiles := get_pidfiles(em.config.rundir)
|
||||||
|
for _, entry in em.config.entries {
|
||||||
|
if entry.pidfile != '' && entry.pidfile !in pidfiles {
|
||||||
|
pidfiles << entry.pidfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mut running_entries := []Entry{}
|
||||||
|
for pidfile in pidfiles {
|
||||||
|
pid := read_pidfile(pidfile)
|
||||||
|
if os.exists(os.join_path_single('/proc', pid.str())) {
|
||||||
|
entry_name := os.file_name(pidfile).split('.')[0]
|
||||||
|
mut entry := em.by_name(entry_name) or { Entry{} }
|
||||||
|
if entry.name != '' {
|
||||||
|
entry.pid = pid
|
||||||
|
running_entries << entry
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
os.rm(pidfile) or {} // FIXME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return running_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (em EntryManager) is_running(name string) !bool {
|
||||||
|
entry := em.by_name(name) or { return err }
|
||||||
|
pidfile := get_pidfile_path(rundir: em.config.rundir, entry: entry.name, path: entry.pidfile)
|
||||||
|
if os.exists(pidfile) {
|
||||||
|
pid := read_pidfile(pidfile)
|
||||||
|
if os.exists(os.join_path_single('/proc', pid.str())) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
os.rm(pidfile) or { return err }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
110
src/loadconf.v
Normal file
110
src/loadconf.v
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import os
|
||||||
|
import log
|
||||||
|
import maps
|
||||||
|
import toml { Any }
|
||||||
|
|
||||||
|
pub struct Entry {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
pid int @[json: '-']
|
||||||
|
pub mut:
|
||||||
|
exec []string
|
||||||
|
env map[string]string
|
||||||
|
pidfile string
|
||||||
|
workdir string
|
||||||
|
labels []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub mut:
|
||||||
|
rundir string
|
||||||
|
include []string
|
||||||
|
entries map[string]Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut c Config) from_toml(any Any) {
|
||||||
|
all := any.as_map()
|
||||||
|
c.rundir = all.value('rundir').default_to(runtime_dir).string()
|
||||||
|
c.include = all.value('include').default_to([]Any{}).array().as_strings()
|
||||||
|
entries := all['entry'] or { Any(map[string]Any{}) }.as_map()
|
||||||
|
for k, v in entries {
|
||||||
|
entry := {
|
||||||
|
k: Entry{
|
||||||
|
name: k
|
||||||
|
exec: v.as_map().value('exec').default_to([]Any{}).array().as_strings()
|
||||||
|
env: v.as_map().value('env').default_to(map[string]Any{}).as_map().as_strings()
|
||||||
|
pidfile: v.as_map().value('pidfile').default_to('').string()
|
||||||
|
workdir: v.as_map().value('workdir').default_to('').string()
|
||||||
|
labels: v.as_map().value('labels').default_to([]Any{}).array().as_strings()
|
||||||
|
description: v.as_map().value('description').default_to('').string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maps.merge_in_place(mut c.entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime_dir = get_runtime_dir()
|
||||||
|
|
||||||
|
fn get_runtime_dir() string {
|
||||||
|
return os.getenv_opt('XDG_RUNTIME_DIR') or {
|
||||||
|
if os.geteuid() == 0 {
|
||||||
|
if os.exists('/run') {
|
||||||
|
return '/run'
|
||||||
|
} else {
|
||||||
|
return '/var/run'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir := os.temp_dir()
|
||||||
|
log.warn('XDG_RUNTIME_DIR is unset, fallback to ${dir} for PID-files')
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config(config_path string) Config {
|
||||||
|
filepath := os.abs_path(os.expand_tilde_to_home(os.norm_path(config_path)))
|
||||||
|
mut conf := Config{}
|
||||||
|
return load_config_recursively(mut conf, 0, filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const max_recursion_depth = $d('pt_max_recursion_depth', 10)
|
||||||
|
|
||||||
|
fn load_config_recursively(mut conf Config, recursion_depth int, file string) Config {
|
||||||
|
mut recursion := recursion_depth + 1
|
||||||
|
if recursion > max_recursion_depth {
|
||||||
|
log.warn('Max recursion depth reached, ${file} is not loaded')
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
log.debug('Loading config file ${file}')
|
||||||
|
text := os.read_file(file) or {
|
||||||
|
log.error('Unable to read file ${file}: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
loaded := toml.decode[Config](text) or {
|
||||||
|
log.error('Unable to parse config file ${file}: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
if recursion == 1 {
|
||||||
|
conf.rundir = loaded.rundir // disallow rundir overriding
|
||||||
|
}
|
||||||
|
conf.include = loaded.include
|
||||||
|
maps.merge_in_place(mut conf.entries, loaded.entries)
|
||||||
|
if conf.include.len != 0 {
|
||||||
|
mut matched_files := []string{}
|
||||||
|
old_cwd := os.getwd()
|
||||||
|
os.chdir(os.dir(file)) or {}
|
||||||
|
for glob in conf.include {
|
||||||
|
matched_files << os.glob(os.expand_tilde_to_home(glob)) or { [] }
|
||||||
|
}
|
||||||
|
for filepath in matched_files {
|
||||||
|
if os.is_dir(filepath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conf = load_config_recursively(mut conf, recursion, os.abs_path(filepath))
|
||||||
|
}
|
||||||
|
os.chdir(old_cwd) or {}
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
205
src/main.v
Normal file
205
src/main.v
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import os
|
||||||
|
import log
|
||||||
|
import cli { Command, Flag }
|
||||||
|
import x.json2 as json
|
||||||
|
|
||||||
|
const default_config_file = $d('pt_default_config_file', '~/.ptrc')
|
||||||
|
|
||||||
|
fn init(cmd Command) Config {
|
||||||
|
debug := cmd.flags.get_bool('debug') or { false }
|
||||||
|
config := cmd.flags.get_string('config') or { default_config_file }
|
||||||
|
if debug {
|
||||||
|
log.set_level(.debug)
|
||||||
|
}
|
||||||
|
return load_config(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_command(cmd Command) ! {
|
||||||
|
println(cmd.help_message())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ls_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
output := cmd.flags.get_string('output')!
|
||||||
|
match output {
|
||||||
|
'json' {
|
||||||
|
mut entries := []Entry{}
|
||||||
|
for _, val in conf.entries {
|
||||||
|
entries << val
|
||||||
|
}
|
||||||
|
println(json.encode[[]Entry](entries))
|
||||||
|
}
|
||||||
|
'brief' {
|
||||||
|
for key, _ in conf.entries {
|
||||||
|
println(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for key, val in conf.entries {
|
||||||
|
println('${key:-24}${val.description:01}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
em := EntryManager.new(conf)
|
||||||
|
labels := cmd.flags.get_strings('label') or { []string{} }
|
||||||
|
mut code := 0
|
||||||
|
if labels.len > 0 {
|
||||||
|
if cmd.args.len > 0 {
|
||||||
|
log.warn('Positional arguments are ignored: ${cmd.args}')
|
||||||
|
}
|
||||||
|
for entry in em.by_labels(labels) {
|
||||||
|
em.run(entry) or {
|
||||||
|
log.error(err.str())
|
||||||
|
code = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cmd.args.len > 0 {
|
||||||
|
for arg in cmd.args {
|
||||||
|
entry := em.by_name(arg) or {
|
||||||
|
log.error(err.str())
|
||||||
|
code = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
em.run(entry) or {
|
||||||
|
log.error(err.str())
|
||||||
|
code = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ps_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
em := EntryManager.new(conf)
|
||||||
|
entries := em.processes()
|
||||||
|
println('PID NAME')
|
||||||
|
for entry in entries {
|
||||||
|
println('${entry.pid:-12}${entry.name}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signal_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
em := EntryManager.new(conf)
|
||||||
|
if cmd.args.len < 2 {
|
||||||
|
println(cmd.help_message())
|
||||||
|
}
|
||||||
|
sig := signal_from_string(cmd.args[0]) or {
|
||||||
|
log.error(err.str())
|
||||||
|
exit(2)
|
||||||
|
}
|
||||||
|
for entry in cmd.args[1..] {
|
||||||
|
em.signal(entry, sig) or {
|
||||||
|
log.error(err.str())
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labels_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
em := EntryManager.new(conf)
|
||||||
|
for label in em.labels().sorted() {
|
||||||
|
println(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_command(cmd Command) ! {
|
||||||
|
conf := init(cmd)
|
||||||
|
em := EntryManager.new(conf)
|
||||||
|
for entry in cmd.args {
|
||||||
|
em.signal(entry, .term) or {
|
||||||
|
log.error(err.str())
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
mut app := Command{
|
||||||
|
name: 'pt'
|
||||||
|
execute: root_command
|
||||||
|
version: $d('pt_version', '0.0.0')
|
||||||
|
sort_commands: true
|
||||||
|
defaults: struct {
|
||||||
|
man: false
|
||||||
|
}
|
||||||
|
flags: [
|
||||||
|
Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'config'
|
||||||
|
abbrev: 'c'
|
||||||
|
description: 'Config file path.'
|
||||||
|
global: true
|
||||||
|
default_value: [default_config_file]
|
||||||
|
},
|
||||||
|
Flag{
|
||||||
|
flag: .bool
|
||||||
|
name: 'debug'
|
||||||
|
description: 'Enable debug logs.'
|
||||||
|
global: true
|
||||||
|
},
|
||||||
|
]
|
||||||
|
commands: [
|
||||||
|
Command{
|
||||||
|
name: 'ls'
|
||||||
|
execute: ls_command
|
||||||
|
description: 'List defined command entries.'
|
||||||
|
flags: [
|
||||||
|
Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'output'
|
||||||
|
abbrev: 'o'
|
||||||
|
description: 'Set output format [text, json, brief].'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Command{
|
||||||
|
name: 'start'
|
||||||
|
usage: '[<entry>]...'
|
||||||
|
execute: start_command
|
||||||
|
description: 'Start entries by name or label.'
|
||||||
|
flags: [
|
||||||
|
Flag{
|
||||||
|
flag: .string_array
|
||||||
|
name: 'label'
|
||||||
|
abbrev: 'l'
|
||||||
|
description: 'Select entries by label. Can be multiple.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Command{
|
||||||
|
name: 'ps'
|
||||||
|
execute: ps_command
|
||||||
|
description: 'Print running entries list.'
|
||||||
|
},
|
||||||
|
Command{
|
||||||
|
name: 'signal'
|
||||||
|
usage: '<SIGNAL> <entry> [<entry>]...'
|
||||||
|
execute: signal_command
|
||||||
|
description: 'Send OS signal to running entry.'
|
||||||
|
},
|
||||||
|
Command{
|
||||||
|
name: 'labels'
|
||||||
|
execute: labels_command
|
||||||
|
description: 'List all entry labels.'
|
||||||
|
},
|
||||||
|
Command{
|
||||||
|
name: 'stop'
|
||||||
|
usage: '[<entry>]...'
|
||||||
|
execute: stop_command
|
||||||
|
description: 'Stop entries.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
app.setup()
|
||||||
|
app.parse(os.args)
|
||||||
|
}
|
46
src/pidfile.v
Normal file
46
src/pidfile.v
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import os
|
||||||
|
import log
|
||||||
|
|
||||||
|
fn read_pidfile(path string) int {
|
||||||
|
log.debug('Read PID file: ${path}')
|
||||||
|
pid := os.read_file(os.norm_path(path)) or {
|
||||||
|
log.error('Unable to read PID file: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
return pid.i32()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_pidfile(path string, pid int) {
|
||||||
|
log.debug("Write PID file '${path}' for process ${pid}")
|
||||||
|
filepath := os.norm_path(path)
|
||||||
|
os.mkdir_all(os.dir(filepath)) or {
|
||||||
|
log.error('Cannot create dirs ${filepath}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
os.write_file(filepath, pid.str()) or {
|
||||||
|
log.error('Cannot write PID file: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const piddir = $d('pt_piddir', 'pt')
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
struct PidfileParams {
|
||||||
|
rundir string
|
||||||
|
entry string // entry.name
|
||||||
|
path string // entry.pidfile
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pidfile_path(params PidfileParams) string {
|
||||||
|
if params.path != '' {
|
||||||
|
return params.path
|
||||||
|
}
|
||||||
|
return os.join_path(params.rundir, piddir, params.entry + '.pid')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pidfiles(rundir string) []string {
|
||||||
|
return os.glob(os.join_path(rundir, piddir, '*.pid')) or { []string{} }
|
||||||
|
}
|
20
src/signal.c.v
Normal file
20
src/signal.c.v
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import os
|
||||||
|
import log
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
fn send_signal(pid int, signal os.Signal) {
|
||||||
|
log.debug('Send ${signal} to process ${pid}')
|
||||||
|
C.kill(pid, int(signal))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signal_from_string(signal string) !os.Signal {
|
||||||
|
sig := signal.to_lower().trim_string_left('sig')
|
||||||
|
if sig.is_int() {
|
||||||
|
return os.Signal.from(sig.int()) or { return error('Invalid signal ${sig}') }
|
||||||
|
} else {
|
||||||
|
return os.Signal.from_string(sig) or { return error('Invalid signal ${sig}') }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user