2
3
의 미러 https://github.com/sammy-ette/Hilbish synced 2025-08-10 02:52:03 +00:00

feat: add job hooks (part of #109)

This commit is contained in:
TorchedSammy 2022-03-19 13:10:50 -04:00
부모 63bc398f1c
커밋 1378a74e87
로그인 계정: sammyette
GPG 키 ID: 904FC49417B44DCD
4개의 변경된 파일220개의 추가작업 그리고 3개의 파일을 삭제

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

12
docs/hooks/job.txt Normal file
파일 보기

@ -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.

134
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,15 +155,138 @@ 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)
return err
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

75
job.go Normal file
파일 보기

@ -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]
}