init
This commit is contained in:
		
							
								
								
									
										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}') }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user