mirror of https://github.com/Hilbis/Hilbish
feat: add job hooks (part of #109)
parent
63bc398f1c
commit
1378a74e87
2
api.go
2
api.go
|
@ -106,6 +106,8 @@ Check out the {blue}{bold}guide{reset} command to get started.
|
|||
util.Document(L, hshcomp, "Completions interface for Hilbish.")
|
||||
L.SetField(mod, "completion", hshcomp)
|
||||
|
||||
jobs = newJobHandler()
|
||||
|
||||
L.Push(mod)
|
||||
|
||||
return 1
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
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.
|
||||
|
||||
+ `job.start` -> job > Thrown when a new background job starts.
|
||||
|
||||
+ `job.done` -> job > Thrown when a background jobs exits.
|
||||
|
132
exec.go
132
exec.go
|
@ -1,13 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hilbish/util"
|
||||
|
@ -17,6 +20,7 @@ import (
|
|||
//"github.com/yuin/gopher-lua/parse"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
)
|
||||
|
||||
var errNotExec = errors.New("not executable")
|
||||
|
@ -92,6 +96,7 @@ func execCommand(cmd, old string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var bg bool
|
||||
exechandle := func(ctx context.Context, args []string) error {
|
||||
_, argstring := splitInput(strings.Join(args, " "))
|
||||
// i dont really like this but it works
|
||||
|
@ -150,16 +155,139 @@ func execCommand(cmd, old string) error {
|
|||
return interp.NewExitStatus(127)
|
||||
}
|
||||
|
||||
return interp.DefaultExecHandler(2 * time.Second)(ctx, args)
|
||||
killTimeout := 2 * time.Second
|
||||
// from here is basically copy-paste of the default exec handler from
|
||||
// sh/interp but with our job handling
|
||||
hc := interp.HandlerCtx(ctx)
|
||||
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintln(hc.Stderr, err)
|
||||
return interp.NewExitStatus(127)
|
||||
}
|
||||
|
||||
env := hc.Env
|
||||
envList := make([]string, 0, 64)
|
||||
env.Each(func(name string, vr expand.Variable) bool {
|
||||
if !vr.IsSet() {
|
||||
// If a variable is set globally but unset in the
|
||||
// runner, we need to ensure it's not part of the final
|
||||
// list. Seems like zeroing the element is enough.
|
||||
// This is a linear search, but this scenario should be
|
||||
// rare, and the number of variables shouldn't be large.
|
||||
for i, kv := range envList {
|
||||
if strings.HasPrefix(kv, name+"=") {
|
||||
envList[i] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if vr.Exported && vr.Kind == expand.String {
|
||||
envList = append(envList, name+"="+vr.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
cmd := exec.Cmd{
|
||||
Path: path,
|
||||
Args: args,
|
||||
Env: envList,
|
||||
Dir: hc.Dir,
|
||||
Stdin: hc.Stdin,
|
||||
Stdout: hc.Stdout,
|
||||
Stderr: hc.Stderr,
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
job := jobs.getLatest()
|
||||
if err == nil {
|
||||
if bg {
|
||||
job.start(cmd.Process.Pid)
|
||||
}
|
||||
|
||||
if done := ctx.Done(); done != nil {
|
||||
go func() {
|
||||
<-done
|
||||
|
||||
if killTimeout <= 0 || runtime.GOOS == "windows" {
|
||||
cmd.Process.Signal(os.Kill)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: don't temporarily leak this goroutine
|
||||
// if the program stops itself with the
|
||||
// interrupt.
|
||||
go func() {
|
||||
time.Sleep(killTimeout)
|
||||
cmd.Process.Signal(os.Kill)
|
||||
}()
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
}()
|
||||
}
|
||||
|
||||
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:
|
||||
if bg {
|
||||
job.exitCode = int(exit)
|
||||
job.finish()
|
||||
}
|
||||
return interp.NewExitStatus(exit)
|
||||
}
|
||||
|
||||
runner, _ := interp.New(
|
||||
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
|
||||
interp.ExecHandler(exechandle),
|
||||
)
|
||||
err = runner.Run(context.TODO(), file)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
printer := syntax.NewPrinter()
|
||||
|
||||
for _, stmt := range file.Stmts {
|
||||
bg = false
|
||||
if stmt.Background {
|
||||
bg = true
|
||||
printer.Print(buf, stmt.Cmd)
|
||||
|
||||
stmtStr := buf.String()
|
||||
buf.Reset()
|
||||
jobs.add(stmtStr)
|
||||
}
|
||||
|
||||
err = runner.Run(context.TODO(), stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
|
||||
var skip []string
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var jobs *jobHandler
|
||||
|
||||
type job struct {
|
||||
cmd string
|
||||
running bool
|
||||
id int
|
||||
pid int
|
||||
exitCode int
|
||||
}
|
||||
|
||||
func (j *job) start(pid int) {
|
||||
j.pid = pid
|
||||
j.running = true
|
||||
hooks.Em.Emit("job.start", j.lua())
|
||||
}
|
||||
|
||||
func (j *job) finish() {
|
||||
j.running = false
|
||||
hooks.Em.Emit("job.done", j.lua())
|
||||
}
|
||||
|
||||
func (j *job) lua() *lua.LTable {
|
||||
// returns lua table for job
|
||||
// because userdata is gross
|
||||
luaJob := l.NewTable()
|
||||
|
||||
l.SetField(luaJob, "cmd", lua.LString(j.cmd))
|
||||
l.SetField(luaJob, "running", lua.LBool(j.running))
|
||||
l.SetField(luaJob, "id", lua.LNumber(j.id))
|
||||
l.SetField(luaJob, "pid", lua.LNumber(j.pid))
|
||||
l.SetField(luaJob, "exitCode", lua.LNumber(j.exitCode))
|
||||
|
||||
return luaJob
|
||||
}
|
||||
|
||||
type jobHandler struct {
|
||||
jobs map[int]*job
|
||||
latestID int
|
||||
mu *sync.RWMutex
|
||||
}
|
||||
|
||||
func newJobHandler() *jobHandler {
|
||||
return &jobHandler{
|
||||
jobs: make(map[int]*job),
|
||||
latestID: 0,
|
||||
mu: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jobHandler) add(cmd string) {
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
|
||||
j.latestID++
|
||||
j.jobs[j.latestID] = &job{
|
||||
cmd: cmd,
|
||||
running: false,
|
||||
id: j.latestID,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jobHandler) getLatest() *job {
|
||||
j.mu.RLock()
|
||||
defer j.mu.RUnlock()
|
||||
|
||||
return j.jobs[j.latestID]
|
||||
}
|
Loading…
Reference in New Issue