Compare commits

..

7 Commits

Author SHA1 Message Date
TorchedSammy d808534da6
perf: use waitgroup instead of loop to wait on timers 2022-05-21 21:58:58 -04:00
TorchedSammy a4b358fd9c
perf: use struct{} for done channel in timer instead of bool 2022-05-21 21:39:14 -04:00
TorchedSammy 8f6f27e647
docs: add docs for lunacolors (closes #144) 2022-05-21 21:28:06 -04:00
TorchedSammy f888aabc80
chore!: update lunacolors 2022-05-21 21:26:34 -04:00
TorchedSammy 2572d0ea9e
docs: update changelog with latest changes 2022-05-21 21:11:59 -04:00
sammyette d2f16dfbbf
feat: job enhancements (#153)
* feat: add hilbish.job.add function

this is mainly to accomodate for the employer
handler (#152)

* feat!: add start function to jobs

the commit itself adds a few things but the
main purpose is to facilitate a lua side start
function that can restart the job

there is a breaking change in the hilbish.job.add
function; it is now required to provide an extra
table for arguments, since the first cmd
table isnt really what's actually ran

* fix: reuse standard files for jobs

* fix: deadlock in lua job add function and not taking proper amount of args

* fix: assign binary path to job

* feat: emit job.add hook when job is added

* chore: update modules

* fix: use setpgid on cmd procattr for background jobs

* fix: use right procattr on correct os

* fix: set bg proc attr in build tagged file

* feat: add disown function

* fix: stop jobs on exit

* feat: add disown command

* feat: add jobs.last function to get last job

* feat: make disown command get last job if id isnt suppied as arg

* chore: remove unused code

* feat: add job output

* chore: fix comments

* fix!: make exec path in job add explicit in lua side

* docs: add docs and changelogs relating to jobs
2022-05-21 20:53:36 -04:00
TorchedSammy 392cb66382
fix: handle sigterm and remove obsolete code 2022-05-21 20:53:02 -04:00
17 changed files with 382 additions and 76 deletions

View File

@ -23,6 +23,26 @@ is for everything/anything as opposed to just adding a single command completion
[#122](https://github.com/Rosettea/Hilbish/issues/122)
- `fs.abs(path)` to get absolute path.
- Nature module (`doc nature`)
- `hilbish.jobs.add(cmdstr, args, execPath)` to add a job to the job table.
`cmdstr` would be user input, `args` is the args for the command (includes arg0)
and `execPath` is absolute path to command executable
- `job.add` hook is thrown when a job is added. acts as a unique hook for
jobs
- `hilbish.jobs.disown(id)` and `disown` builtin to disown a job. `disown`
without arguments will disown the last job.
- `hilbish.jobs.last()` returns the last added job.
- Job output (stdout/stderr) can now be obtained via the `stdout` and `stderr`
fields on a job object.
- Documentation for jobs is now available via `doc jobs`.
- `hilbish.alias.resolve(cmdstr)` to resolve a command alias.
- `hilbish.opts` for shell options. Currently, the only opt is `autocd`.
- `hilbish.editor` interface for interacting with the line editor that
Hilbish uses.
- `hilbish.vim` interface to dynamically get/set vim registers.
Example usage: `hilbish.vim.registers['a'] = 'hello'`. You can also
get the mode with it via `hilbish.vim.mode`
- `hilbish.version` interface for more info about Hilbish's version. This
includes git commit, branch, and (new!!) release name.
### Changed
- **Breaking Change:** Upgraded to Lua 5.4.
@ -33,6 +53,7 @@ This is probably one of (if not the) biggest things in this release.
user input, exit code, and error. User input has been added to the return to
account for runners wanting to prompt for continued input, and to add it
properly to history.
- All `fs` module functions which take paths now implicitly expand ~ to home.
### Fixed
- If in Vim replace mode, input at the end of the line inserts instead of
@ -57,6 +78,16 @@ certain color rules.
- Cursor position with CJK characters. ([#145](https://github.com/Rosettea/Hilbish/pull/145))
- Files with same name as parent folder in completions getting cut off [#136](https://github.com/Rosettea/Hilbish/issues/136))
- `hilbish.which` now works with commanders and aliases.
- Background jobs no longer take stdin so they do not interfere with shell
input.
- Completions are fixed in cases where the query/line is an alias alone
where it can also resolve to the beginning of command names.
(reference [this commit](https://github.com/Rosettea/Hilbish/commit/2790982ad123115c6ddbc5764677fdca27668cea))
for explanation.
- Jobs now throw `job.done` and set running to false when stopped via
Lua `job.stop` function.
- Jobs are always started in sh exec handler now instead of only successful start.
- SIGTERM is handled properly now, which means stopping jobs and timers.
## [1.2.0] - 2022-03-17
### Added

View File

@ -1,11 +1,5 @@
Note: A `job` is a table with the following keys:
- cmd: command string
- running: boolean whether the job is running
- id: unique id for the job
- pid: process id for the job
- exitCode: exit code of the job
In ordinary cases you'd prefer to use the id instead of pid. The id is unique to
Hilbish and is how you get jobs with the `hilbish.jobs` interface.
Note: `job` refers to a job object. YOu can check `doc jobs` for more
detail.
+ `job.start` -> job > Thrown when a new background job starts.

40
docs/jobs.txt 100644
View File

@ -0,0 +1,40 @@
Hilbish has pretty standard job control. It's missing one or two things,
but works well. One thing which is different from other shells
(besides Hilbish) itself is the API for jobs, and of course it's in Lua.
You can add jobs, stop and delete (disown) them and even get output.
# Job Interface
The job interface refers to `hilbish.jobs`.
## Functions
(Note that in the list here, they're called from `hilbish.jobs`, so
a listing of `foo` would mean `hilbish.jobs.foo`)
- `all()` -> {jobs}: Returns a table of all jobs.
- `last()` -> job: Returns the last added job.
- `get(id)` -> job: Get a job by its ID.
- `add(cmdstr, args, execPath)` -> job: Adds a new job to the job table.
Note that this does not run the command; You have to start it manually.
`cmdstr` is the user's input for the job, `args` is a table of arguments
for the command. It includes arg0 (don't set it as entry 0 in the table)
and `execPath` is an absolute path for the command executable.
- `disown(id)`: Removes a job by ID from the job table.
# Job Object
A job object on the Lua side is a table with some functions.
On the under side it represents a job in the job table.
You can still have a job object for a disowned job,
it just won't be *working* anywhere. :^)
## Properties
- `cmd`: command string
- `running`: boolean whether the job is running
- `id`: unique id for the job
- `pid`: process id for the job
- `exitCode`: exit code of the job
In ordinary cases you'd prefer to use the `id` instead of `pid`.
The `id` is unique to Hilbish and is how you get jobs with the
`hilbish.jobs` interface. It may also not describe the job entirely.
## Functions
- `stop()`: Stops the job.
- `start()`: Starts the job.

View File

@ -0,0 +1,34 @@
Lunacolors is an ANSI color/styling library for Lua. It is included
by default in standard Hilbish distributions to provide easy styling
for things like prompts and text.
For simple usage, a single color or style is enough. For example,
you can just use `lunacolors.blue 'Hello world'` and that'll return
blue text which you can print. This includes styles like bold,
underline, etc.
In other usage, you may want to use a format string instead of having
multiple nested functions for different styles. This is where the format
function comes in. You can used named keywords to style a section of text.
The list of arguments are:
Colors:
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
Styles:
- bold
- dim
- italic
- underline
- invert
For the colors, there are background and bright variants. The background
color variants have a suffix of `Bg` and bright has a prefix of `bright`.
Note that appropriate camel casing has to be applied to them. So bright
blue would be `brightBlue` and background cyan would be `cyanBg`.

70
exec.go
View File

@ -239,7 +239,7 @@ func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) {
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr)
jobs.add(stmtStr, []string{}, "")
}
interp.ExecHandler(execHandle(bg))(runner)
@ -357,13 +357,15 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
Stderr: hc.Stderr,
}
err = cmd.Start()
var j *job
if bg {
j = jobs.getLatest()
j.setHandle(cmd.Process)
j.start(cmd.Process.Pid)
j.setHandle(&cmd)
err = j.start()
} else {
err = cmd.Start()
}
if err == nil {
if done := ctx.Done(); done != nil {
go func() {
@ -388,35 +390,8 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
err = cmd.Wait()
}
var exit uint8
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return ctx.Err()
}
exit = uint8(128 + status.Signal())
goto end
}
exit = uint8(status.ExitStatus())
goto end
}
exit = 1
goto end
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
goto end
case nil:
goto end
default:
return err
}
end:
exit := handleExecErr(err)
if bg {
j.exitCode = int(exit)
j.finish()
@ -425,6 +400,35 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
}
}
func handleExecErr(err error) (exit uint8) {
ctx := context.TODO()
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return
}
exit = uint8(128 + status.Signal())
return
}
exit = uint8(status.ExitStatus())
return
}
exit = 1
return
case *exec.Error:
// did not start
//fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
default: return
}
return
}
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
var skip []string
if runtime.GOOS == "windows" {

View File

@ -4,8 +4,13 @@ package main
import (
"os"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func findExecutable(path string, inPath, dirs bool) error {
f, err := os.Stat(path)
if err != nil {

View File

@ -5,8 +5,13 @@ package main
import (
"path/filepath"
"os"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
func findExecutable(path string, inPath, dirs bool) error {
nameExt := filepath.Ext(path)
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))

2
go.mod
View File

@ -29,4 +29,4 @@ replace github.com/maxlandon/readline => ./readline
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3

2
go.sum
View File

@ -1,5 +1,7 @@
github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac h1:dtXrgjch8PQyf7C90anZUquB5U3dr8AcMGJofeuirrI=
github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3 h1:I/wWr40FFLFF9pbT3wLb1FAEZhKb/hUWE+nJ5uHBK2g=
github.com/Rosettea/golua v0.0.0-20220518005949-116371948fe3/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e h1:P2XupP8SaylWaudD1DqbWtZ3mIa8OsE9635LmR+Q+lg=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=

187
job.go
View File

@ -1,8 +1,13 @@
package main
import (
"sync"
"bytes"
"errors"
"io"
"os"
"os/exec"
"sync"
"syscall"
"hilbish/util"
@ -17,18 +22,59 @@ type job struct {
id int
pid int
exitCode int
proc *os.Process
once bool
args []string
// save path for a few reasons, one being security (lmao) while the other
// would just be so itll be the same binary command always (path changes)
path string
handle *exec.Cmd
cmdout io.Writer
cmderr io.Writer
stdout *bytes.Buffer
stderr *bytes.Buffer
}
func (j *job) start(pid int) {
j.pid = pid
func (j *job) start() error {
if j.handle == nil || j.once {
// cmd cant be reused so make a new one
cmd := exec.Cmd{
Path: j.path,
Args: j.args,
}
j.setHandle(&cmd)
}
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (sigint)
// not go to it (child process)
j.handle.SysProcAttr = bgProcAttr
// reset output buffers
j.stdout.Reset()
j.stderr.Reset()
// make cmd write to both standard output and output buffers for lua access
j.handle.Stdout = io.MultiWriter(j.cmdout, j.stdout)
j.handle.Stderr = io.MultiWriter(j.cmderr, j.stderr)
if !j.once {
j.once = true
}
err := j.handle.Start()
proc := j.getProc()
j.pid = proc.Pid
j.running = true
hooks.Em.Emit("job.start", j.lua())
return err
}
func (j *job) stop() {
// finish will be called in exec handle
j.proc.Kill()
proc := j.getProc()
if proc != nil {
proc.Kill()
}
}
func (j *job) finish() {
@ -36,13 +82,35 @@ func (j *job) finish() {
hooks.Em.Emit("job.done", j.lua())
}
func (j *job) setHandle(handle *os.Process) {
j.proc = handle
func (j *job) wait() {
j.handle.Wait()
}
func (j *job) setHandle(handle *exec.Cmd) {
j.handle = handle
j.args = handle.Args
j.path = handle.Path
if handle.Stdout != nil {
j.cmdout = handle.Stdout
}
if handle.Stderr != nil {
j.cmderr = handle.Stderr
}
}
func (j *job) getProc() *os.Process {
handle := j.handle
if handle != nil {
return handle.Process
}
return nil
}
func (j *job) lua() rt.Value {
jobFuncs := map[string]util.LuaExport{
"stop": {j.luaStop, 0, false},
"start": {j.luaStart, 0, false},
}
luaJob := rt.NewTable()
util.SetExports(l, luaJob, jobFuncs)
@ -52,10 +120,23 @@ func (j *job) lua() rt.Value {
luaJob.Set(rt.StringValue("id"), rt.IntValue(int64(j.id)))
luaJob.Set(rt.StringValue("pid"), rt.IntValue(int64(j.pid)))
luaJob.Set(rt.StringValue("exitCode"), rt.IntValue(int64(j.exitCode)))
luaJob.Set(rt.StringValue("stdout"), rt.StringValue(string(j.stdout.Bytes())))
luaJob.Set(rt.StringValue("stderr"), rt.StringValue(string(j.stderr.Bytes())))
return rt.TableValue(luaJob)
}
func (j *job) luaStart(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if !j.running {
err := j.start()
exit := handleExecErr(err)
j.exitCode = int(exit)
j.finish()
}
return c.Next(), nil
}
func (j *job) luaStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if j.running {
j.stop()
@ -79,7 +160,7 @@ func newJobHandler() *jobHandler {
}
}
func (j *jobHandler) add(cmd string) *job {
func (j *jobHandler) add(cmd string, args []string, path string) *job {
j.mu.Lock()
defer j.mu.Unlock()
@ -88,8 +169,15 @@ func (j *jobHandler) add(cmd string) *job {
cmd: cmd,
running: false,
id: j.latestID,
args: args,
path: path,
cmdout: os.Stdout,
cmderr: os.Stderr,
stdout: &bytes.Buffer{},
stderr: &bytes.Buffer{},
}
j.jobs[j.latestID] = jb
hooks.Em.Emit("job.add", jb.lua())
return jb
}
@ -101,11 +189,41 @@ func (j *jobHandler) getLatest() *job {
return j.jobs[j.latestID]
}
func (j *jobHandler) disown(id int) error {
j.mu.RLock()
if j.jobs[id] == nil {
return errors.New("job doesnt exist")
}
j.mu.RUnlock()
j.mu.Lock()
delete(j.jobs, id)
j.mu.Unlock()
return nil
}
func (j *jobHandler) stopAll() {
j.mu.RLock()
defer j.mu.RUnlock()
for _, jb := range j.jobs {
// on exit, unix shell should send sighup to all jobs
if jb.running {
proc := jb.getProc()
proc.Signal(syscall.SIGHUP)
jb.wait() // waits for program to exit due to sighup
}
}
}
func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table {
jobFuncs := map[string]util.LuaExport{
"all": {j.luaAllJobs, 0, false},
"last": {j.luaLastJob, 0, false},
"get": {j.luaGetJob, 1, false},
"add": {j.luaAddJob, 1, false},
"add": {j.luaAddJob, 3, false},
"disown": {j.luaDisownJob, 1, false},
}
luaJob := rt.NewTable()
@ -135,18 +253,30 @@ func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
}
func (j *jobHandler) luaAddJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
if err := c.Check1Arg(); err != nil {
if err := c.CheckNArgs(3); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
largs, err := c.TableArg(1)
if err != nil {
return nil, err
}
execPath, err := c.StringArg(2)
if err != nil {
return nil, err
}
jb := j.add(cmd)
var args []string
util.ForEach(largs, func(k rt.Value, v rt.Value) {
if v.Type() == rt.StringType {
args = append(args, v.AsString())
}
})
jb := j.add(cmd, args, execPath)
return c.PushingNext1(t.Runtime, jb.lua()), nil
}
@ -162,3 +292,32 @@ func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil
}
func (j *jobHandler) luaDisownJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
jobID, err := c.IntArg(0)
if err != nil {
return nil, err
}
err = j.disown(int(jobID))
if err != nil {
return nil, err
}
return c.Next(), nil
}
func (j *jobHandler) luaLastJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
job := j.jobs[j.latestID]
if job == nil { // incase we dont have any jobs yet
return c.Next(), nil
}
return c.PushingNext1(t.Runtime, job.lua()), nil
}

@ -1 +1 @@
Subproject commit b362397a83e4516415c809c7d690b52e79a95f6e
Subproject commit d60cd77c73875b5bb55e5a2fdc30bae01a7ac499

15
main.go
View File

@ -221,6 +221,8 @@ input:
}
fmt.Printf("\u001b[7m∆\u001b[0m" + strings.Repeat(" ", termwidth - 1) + "\r")
}
exit(0)
}
func continuePrompt(prev string) (string, error) {
@ -294,12 +296,15 @@ func contains(s []string, e string) bool {
}
func exit(code int) {
// wait for all timers to finish before exiting
for {
if timers.running == 0 {
os.Exit(code)
}
jobs.stopAll()
// wait for all timers to finish before exiting.
// only do that when not interactive
if !interactive {
timers.wait()
}
os.Exit(code)
}
func getVersion() string {

View File

@ -0,0 +1,25 @@
local commander = require 'commander'
commander.register('disown', function(args)
if #hilbish.jobs.all() == 0 then
print 'disown: no current job'
return 1
end
local id
if #args < 0 then
id = tonumber(args[1])
if not id then
print 'disown: invalid id for job'
return 1
end
else
id = hilbish.jobs.last().id
end
local ok = pcall(hilbish.jobs.disown, id)
if not ok then
print 'disown: job does not exist'
return 2
end
end)

View File

@ -4,3 +4,4 @@ require 'nature.commands.cdr'
require 'nature.commands.doc'
require 'nature.commands.exit'
require 'nature.commands.guide'
require 'nature.commands.disown'

View File

@ -10,20 +10,13 @@ import (
func handleSignals() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGWINCH, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGQUIT)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGWINCH, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGQUIT)
for s := range c {
switch s {
case os.Interrupt:
hooks.Em.Emit("signal.sigint")
if !running && interactive {
lr.ClearInput()
}
case syscall.SIGWINCH:
hooks.Em.Emit("signal.resize")
if !running && interactive {
lr.Resize()
}
case os.Interrupt: hooks.Em.Emit("signal.sigint")
case syscall.SIGTERM: exit(0)
case syscall.SIGWINCH: hooks.Em.Emit("signal.resize")
case syscall.SIGUSR1: hooks.Em.Emit("signal.sigusr1")
case syscall.SIGUSR2: hooks.Em.Emit("signal.sigusr2")
}

View File

@ -25,7 +25,7 @@ type timer struct{
fun *rt.Closure
th *timerHandler
ticker *time.Ticker
channel chan bool
channel chan struct{}
}
func (t *timer) start() error {
@ -35,6 +35,7 @@ func (t *timer) start() error {
t.running = true
t.th.running++
t.th.wg.Add(1)
t.ticker = time.NewTicker(t.dur)
go func() {
@ -65,9 +66,10 @@ func (t *timer) stop() error {
return errors.New("timer not running")
}
t.channel <- true
t.channel <- struct{}{}
t.running = false
t.th.running--
t.th.wg.Done()
return nil
}

View File

@ -12,6 +12,7 @@ import (
var timers *timerHandler
type timerHandler struct {
mu *sync.RWMutex
wg *sync.WaitGroup
timers map[int]*timer
latestID int
running int
@ -22,9 +23,14 @@ func newTimerHandler() *timerHandler {
timers: make(map[int]*timer),
latestID: 0,
mu: &sync.RWMutex{},
wg: &sync.WaitGroup{},
}
}
func (th *timerHandler) wait() {
th.wg.Wait()
}
func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer {
th.mu.Lock()
defer th.mu.Unlock()
@ -34,7 +40,7 @@ func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure
typ: typ,
fun: fun,
dur: dur,
channel: make(chan bool, 1),
channel: make(chan struct{}, 1),
th: th,
id: th.latestID,
}