From 44521f306fa02741b94dcd037dcd8b75f3b317bc Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 20 Jul 2020 09:16:05 -0700 Subject: [PATCH 1/9] improvements for tab complete --- util/shell_complete.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/util/shell_complete.go b/util/shell_complete.go index a6e3289..2d41440 100644 --- a/util/shell_complete.go +++ b/util/shell_complete.go @@ -59,7 +59,16 @@ func ShellCompleter(line string, vt ast.VarTable, ft ast.FuncTable) []string { } } else { for _, f := range fobjs { - compSource = append(compSource, dir + "/" + f.Name()) + fileTok := f.Name() + if dir != "." { + fileTok = dir + "/" + fileTok + } + + if f.IsDir() { + fileTok = fileTok + "/" + } + + compSource = append(compSource, fileTok) } } From e57a97c6010dfd8d46cf671d38f85dd55ed31807 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 20 Jul 2020 09:42:18 -0700 Subject: [PATCH 2/9] update readme --- Readme.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 63d0b30..448e0b5 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ Syntactically Homogeneous Shell ## Overview -This shell was created to have extremely simple syntax. S-Expressions were chosen to represent statements and the scope of language features were constrained to what could be considered practical for daily shell use. This program is meant to be practical for administrators and daily power users. +This shell was created to have extremely simple syntax. S-Expressions were chosen to represent statements and the scope of language features were constrained to what could be considered practical for daily shell use. This program is meant to be practical for administrators and daily power users. It can be used for both system administration (as one might use bash or zsh) as well as for the creation of simple programs. ## Basic Syntax When in doubt the `print_ast` utility can be used to examine the output of the Lex+Parse process. Here you can spot any bugs regarding syntax. @@ -94,9 +94,16 @@ Here is an example of a shs configuration file: (func _SH_PROMPT () (concat (?) ($ basename ($ pwd)) "\n")) ``` +## The Docs +- [Documentation on language interpreter](https://godoc.org/gitlab.com/whom/shs/ast) +- [Documentation on logging module](https://godoc.org/gitlab.com/whom/shs/log) +- [Documentation on configuration module](https://godoc.org/gitlab.com/whom/shs/config) +- [Documentation on utility functions](https://godoc.org/gitlab.com/whom/shs/util) +- [Documentation on standard library](https://godoc.org/gitlab.com/whom/shs/stdlib) +- [Guide to standard library](https://gitlab.com/whom/shs/-/blob/master/stdlib/Readme.md) + ## Contributing - Any contribution to this software is welcome as long as it adheres to the conduct guidelines specified in the `Contributing.md` file in this repository. -- Consider reading the [STDLIB Readme](https://git.callpipe.com/aidan/shs/-/blob/master/stdlib/Readme.md) for more information on how to extend this project. ## License Copyright (C) 2019 Aidan Hahn. From 08bb7073cdab954c7176e1b91118483e1b9b315b Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 22 Jul 2020 00:22:39 -0700 Subject: [PATCH 3/9] incomplete work: big call lib refactor --- cmd/shs.go | 4 + cmd/test_shell.go | 36 +++++++ go.mod | 1 + go.sum | 2 + resources/icon/icon.kra | Bin 0 -> 49587 bytes stdlib/{call.go => shell.go} | 177 ++++++++++++++++++++++++----------- 6 files changed, 163 insertions(+), 57 deletions(-) create mode 100644 cmd/test_shell.go create mode 100644 resources/icon/icon.kra rename stdlib/{call.go => shell.go} (76%) diff --git a/cmd/shs.go b/cmd/shs.go index 3379c7c..cd2087a 100644 --- a/cmd/shs.go +++ b/cmd/shs.go @@ -25,6 +25,7 @@ import ( "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/util" + "gitlab.com/whom/shs/stdlib" "gitlab.com/whom/shs/config" ) @@ -124,6 +125,9 @@ func main() { defer histFile.Close() } + stdlib.InitShellFeatures() + defer stdlib.TeardownShell() + for { setLogLvl(vars) var prePrompt string diff --git a/cmd/test_shell.go b/cmd/test_shell.go new file mode 100644 index 0000000..f783dc7 --- /dev/null +++ b/cmd/test_shell.go @@ -0,0 +1,36 @@ +/* SHS: Syntactically Homogeneous Shell + * Copyright (C) 2019 Aidan Hahn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "fmt" + "time" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/stdlib" +) + +func main() { + log.SetLogLvl(3) + stdlib.InitShellFeatures() + stdlib.LaunchProcess("/usr/bin/vim", []string{"test.txt"}, false) + + fmt.Println("process started, now sleeping for 10 sec") + time.Sleep(10 * time.Second) + + stdlib.TeardownShell() +} diff --git a/go.mod b/go.mod index 2b85407..63a9008 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.14 require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/peterh/liner v1.2.0 // indirect + golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 ) diff --git a/go.sum b/go.sum index 285b298..ee825b8 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,5 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/peterh/liner v1.2.0 h1:w/UPXyl5GfahFxcTOz2j9wCIHNI+pUPr2laqpojKNCg= github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 h1:gVCS+QOncANNPlmlO1AhlU3oxs4V9z+gTtPwIk3p2N8= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/resources/icon/icon.kra b/resources/icon/icon.kra new file mode 100644 index 0000000000000000000000000000000000000000..3b77b37df949706e95d56b997f44346df761c553 GIT binary patch literal 49587 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 literal 0 HcmV?d00001 diff --git a/stdlib/call.go b/stdlib/shell.go similarity index 76% rename from stdlib/call.go rename to stdlib/shell.go index 99e0647..83f7c13 100644 --- a/stdlib/call.go +++ b/stdlib/shell.go @@ -19,30 +19,135 @@ package stdlib import ( "os" - "fmt" - "bytes" - "strconv" + "errors" "os/exec" + "context" "syscall" - "os/signal" - "gitlab.com/whom/shs/ast" + "golang.org/x/sys/unix" "gitlab.com/whom/shs/log" ) -var bgProcs = make([]*exec.Cmd, 0) -var sigs = []os.Signal{ - os.Interrupt, - syscall.SIGTERM, - syscall.SIGTSTP, - syscall.SIGTTIN, - syscall.SIGTTOU, - syscall.SIGCONT, +func tcsetpgrp(fd int, pgrp int) error { + return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pgrp) +} + +var sigChan chan os.Signal +var waitChan chan error + +/* mapping of pid to Proc + */ +var JobMap = map[int]Proc{} + +/* id of shell process group + */ +var pgid int + +/* Holds an os/exec Cmd object and its context cancel function + */ +type Proc struct { + Ctl *exec.Cmd + Cancel context.CancelFunc +} + +var catchMe = []os.Signal{ + syscall.SIGINT, } /* Exit code of last run process */ var LastExitCode int +// Job control handlers +func signalHandler() { + for { + sig := <- sigChan + switch sig { + case syscall.SIGINT: + log.Log(log.DEBUG, + "caught SIGINT!", + "jobctl") + } + } +} + +func waitHandler() { + for { + w := <- waitChan + exit := 0 + if w != nil { + log.Log(log.ERR, + "Child returned error: " + w.Error(), + "jobctl") + + // something outrageous + exit = -1024 + var e *exec.ExitError + if errors.As(w, &e) { + exit = e.Pid() + } + + LastExitCode = exit + } + } +} + +/* Sets pgid + * Installs a sigchld handler + * returns true if success, false on error + */ +func InitShellFeatures() bool { + // TODO: adjust, make configurable, i dunno + sigChan = make(chan os.Signal, 5) + waitChan = make(chan error, 5) + + go signalHandler() + go waitHandler() + + pid := os.Getpid() + pgid = syscall.Getpgrp() + if pid != pgid { + syscall.Setpgid(0, 0) + } + tcsetpgrp(0, pid) + + return true +} + +/* Tears down session + */ +func TeardownShell() { + close(sigChan) + close(waitChan) + // TODO: Exit all processes in the JobMap +} + +/* Makes and stores a new process in the job control + * uses os/exec Cmd object. sets SysProcAttr.Foreground + * calls Cmd.Start() + */ +func LaunchProcess(path string, args []string, background bool) { + c, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(c, path, args...) + cmd.SysProcAttr = &syscall.SysProcAttr{Foreground: !background, Pgid: pgid} + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + cmd.Start() + pid := cmd.Process.Pid + + // TODO: check for clobber? + JobMap[pid] = Proc{ Ctl: cmd, Cancel: cancel } + go func(){ + waitChan <- cmd.Wait() + }() + + if background { + cmd.Process.Signal(syscall.SIGTSTP) + } +} + /* Takes n arguments (list of tokens generated by lexing a shell command) * Evaluates arguments, but does not err on undefined symbols (note the last arg to Eval(...)) * Executes shell command and returns nil @@ -76,35 +181,7 @@ func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { args = append(args, i.Value()) } - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Env = os.Environ() - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() - - err = cmd.Run() - close(signalChan) - signal.Reset(sigs...) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned unparsable error: " + err.Error(), "call") - } - } - + LaunchProcess(path, args, false) return nil } @@ -139,21 +216,7 @@ func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { args = append(args, i.Value()) } - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Stderr = os.Stderr - - bgProcs = append(bgProcs, cmd) - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - - cmd.Start() - cmd.Process.Signal(syscall.SIGTSTP) + LaunchProcess(path, args, true) return nil } From e098a8812c6f21bd4d085eb1c600ad53f57bbec9 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 22 Jul 2020 21:56:31 -0700 Subject: [PATCH 4/9] fg tasks --- cmd/shs.go | 6 +- stdlib/shell.go | 231 +++++++++++++++++++++++++++--------------------- 2 files changed, 131 insertions(+), 106 deletions(-) diff --git a/cmd/shs.go b/cmd/shs.go index cd2087a..739b917 100644 --- a/cmd/shs.go +++ b/cmd/shs.go @@ -78,6 +78,9 @@ func main() { ast.SyncTablesWithOSEnviron = true ast.ExecWhenFuncUndef = true + stdlib.InitShellFeatures() + defer stdlib.TeardownShell() + vars, funcs := config.InitFromConfig(".shsrc") debug_t := ast.GetVar("SH_DEBUG_MODE", vars) if debug_t != nil { @@ -125,9 +128,6 @@ func main() { defer histFile.Close() } - stdlib.InitShellFeatures() - defer stdlib.TeardownShell() - for { setLogLvl(vars) var prePrompt string diff --git a/stdlib/shell.go b/stdlib/shell.go index 83f7c13..4f8ccf2 100644 --- a/stdlib/shell.go +++ b/stdlib/shell.go @@ -19,18 +19,20 @@ package stdlib import ( "os" + "io" + "fmt" + "bytes" "errors" "os/exec" "context" "syscall" + "strconv" + "os/signal" "golang.org/x/sys/unix" + "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" ) -func tcsetpgrp(fd int, pgrp int) error { - return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pgrp) -} - var sigChan chan os.Signal var waitChan chan error @@ -40,7 +42,7 @@ var JobMap = map[int]Proc{} /* id of shell process group */ -var pgid int +var pgid, pid int /* Holds an os/exec Cmd object and its context cancel function */ @@ -64,7 +66,7 @@ func signalHandler() { switch sig { case syscall.SIGINT: log.Log(log.DEBUG, - "caught SIGINT!", + "caught SIGINT", "jobctl") } } @@ -88,26 +90,60 @@ func waitHandler() { LastExitCode = exit } + + log.Log(log.DEBUG, + "handled sigchld!", + "jobctl") } } +// for some reason not implemented in stdlib +func tcsetpgrp(fd int, pgrp int) error { + return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pgrp) +} + +// wrapper for convenience +func setSigState() { + signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN, syscall.SIGTSTP) + signal.Notify(sigChan, syscall.SIGINT) +} + /* Sets pgid * Installs a sigchld handler * returns true if success, false on error */ func InitShellFeatures() bool { - // TODO: adjust, make configurable, i dunno + // TODO: adjust capacity, make configurable maybe? sigChan = make(chan os.Signal, 5) waitChan = make(chan error, 5) go signalHandler() go waitHandler() - pid := os.Getpid() - pgid = syscall.Getpgrp() - if pid != pgid { - syscall.Setpgid(0, 0) + setSigState() + pid = os.Getpid() + var errr error + pgid, errr = syscall.Getpgid(pid) + if errr != nil { + log.Log(log.ERR, + "Failure to get pgid: " + errr.Error(), + "jobctl") + return false } + + termPgrp, err := unix.IoctlGetInt(0, unix.TIOCGPGRP) + if err != nil { + log.Log(log.ERR, + "Failure to get pgrp: " + err.Error(), + "jobctl") + return false + } + + if pgid != termPgrp { + syscall.Kill(-termPgrp, unix.SIGTTIN) + } + + syscall.Setpgid(0, 0) tcsetpgrp(0, pid) return true @@ -125,26 +161,48 @@ func TeardownShell() { * uses os/exec Cmd object. sets SysProcAttr.Foreground * calls Cmd.Start() */ -func LaunchProcess(path string, args []string, background bool) { +func LaunchProcess( + path string, + args []string, + background bool, + stdin io.Reader, + stdout io.Writer, + stderr io.Writer ) { c, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(c, path, args...) - cmd.SysProcAttr = &syscall.SysProcAttr{Foreground: !background, Pgid: pgid} + cmd.SysProcAttr = &syscall.SysProcAttr{Foreground: !background, Setpgid: true} - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + if stdin == nil { + stdin = os.Stdin + } + + if stdout == nil { + stdout = os.Stdout + } + + if stderr == nil { + stderr = os.Stderr + } + + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr cmd.Start() pid := cmd.Process.Pid // TODO: check for clobber? + // unlikely, but PIDs do reset after a while JobMap[pid] = Proc{ Ctl: cmd, Cancel: cancel } - go func(){ - waitChan <- cmd.Wait() - }() if background { - cmd.Process.Signal(syscall.SIGTSTP) + go func(){ + waitChan <- cmd.Wait() + tcsetpgrp(0, pgid) + }() + } else { + cmd.Wait() + tcsetpgrp(0, pgid) } } @@ -181,7 +239,7 @@ func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { args = append(args, i.Value()) } - LaunchProcess(path, args, false) + LaunchProcess(path, args, false, nil, nil, nil) return nil } @@ -191,6 +249,7 @@ func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { * Example: (bg vim file.txt) */ func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, true) if in == nil { return nil } @@ -216,36 +275,51 @@ func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { args = append(args, i.Value()) } - LaunchProcess(path, args, true) + LaunchProcess(path, args, true, nil, nil, nil) return nil } -/* brings last BG'ed process into the foreground +/* takes one argument: pid of process to foreground * returns nil * * Example: * (bg vim file.txt) - * (fg) + * ( ) + * (fg ) */ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if len(bgProcs) < 1 { + if len(JobMap) < 1 { return nil } - cmd := bgProcs[0] - bgProcs = bgProcs[1:] + in = in.Eval(ft, vt, false) + if in.Tag != ast.NUMBER && in.Tag != ast.STRING { + log.Log(log.ERR, + "must supply a number or string to fg", + "fg") + return nil + } - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() + pid, err := strconv.ParseFloat(in.Value(), 64) + if err != nil { + log.Log(log.ERR, + "value supplied to fg could not be cast to float", + "fg") + return nil + } + ipid := int(pid) + proc, ok := JobMap[ipid] + if !ok { + log.Log(log.ERR, + "Process not found, was it started by this shell?", + "fg") + return nil + } + + cmd := proc.Ctl cmd.Process.Signal(syscall.SIGCONT) - err := cmd.Wait() - close(signalChan) - signal.Reset(sigs...) + err = cmd.Wait() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { LastExitCode = exitError.ExitCode() @@ -258,8 +332,7 @@ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } /* Takes 0 args - * returns a string containing info about current jobs - * returns total jobs as well as their PIDs and place in the bg queue + * returns a list of PIDs * * Example: * (bg ping google.com) @@ -271,22 +344,18 @@ func Jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { Tag: ast.LIST, } - _inner := &ast.Token{ - Tag: ast.STRING, - } - - ret.Direct(_inner) - _inner.Set(fmt.Sprintf("Total: %d", len(bgProcs))) - + var _inner *ast.Token iter := &_inner - for i := 0; i < len(bgProcs); i += 1 { - (*iter).Next = &ast.Token{ + for k := range JobMap { + (*iter) = &ast.Token{ Tag: ast.STRING, } - (*iter).Next.Set(fmt.Sprintf("[%d]: %d", i, bgProcs[i].Process.Pid)) + + (*iter).Set(fmt.Sprintf("%d", k)) iter = &(*iter).Next } + ret.Direct(_inner) return ret } @@ -307,7 +376,7 @@ func ReadCmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - var out bytes.Buffer + out := new(bytes.Buffer) path, err := exec.LookPath(in.Value()) if err != nil { log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") @@ -324,34 +393,7 @@ func ReadCmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { args = append(args, i.Value()) } - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Stdout = &out - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() - - err = cmd.Run() - close(signalChan) - signal.Reset(sigs...) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned error: " + err.Error(), "$") - } - } - + LaunchProcess(path, args, false, nil, out, nil) output := out.String() olen := len(output) if olen > 0 && output[olen - 1] == '\n' { @@ -396,33 +438,16 @@ func Kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - found := false - newBgProcs := []*exec.Cmd{} - for _, i := range bgProcs { - if i.Process.Pid != int(pid) { - newBgProcs = append(newBgProcs, i) - } else { - found = true - err = i.Process.Kill() - if err != nil { - log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", - int(pid), err.Error()), "kill") - newBgProcs = append(newBgProcs, i) - } - } - } - - bgProcs = newBgProcs - - if !found { - // docs say no error on unix systems - proc, _ := os.FindProcess(int(pid)) - err = proc.Kill() - if err != nil { - log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", - int(pid), err.Error()), "kill") - } + proc, ok := JobMap[int(pid)] + if !ok { + log.Log(log.ERR, + "Couldnt find process " + in.Value() + ", was it started by this shell?", + "kill") + return nil } + // if this doesnt work do proc.Ctl.Kill() + proc.Cancel() + delete(JobMap, int(pid)) return nil } From f69532d54ce4482e306865d15f2e831e8c7102c0 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 22 Jul 2020 22:30:56 -0700 Subject: [PATCH 5/9] better handling of interrupt --- stdlib/shell.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/stdlib/shell.go b/stdlib/shell.go index 4f8ccf2..ba12cf5 100644 --- a/stdlib/shell.go +++ b/stdlib/shell.go @@ -44,6 +44,11 @@ var JobMap = map[int]Proc{} */ var pgid, pid int +/* cancel func for current running process + */ +var CurCancel *context.CancelFunc + + /* Holds an os/exec Cmd object and its context cancel function */ type Proc struct { @@ -62,12 +67,13 @@ var LastExitCode int // Job control handlers func signalHandler() { for { - sig := <- sigChan - switch sig { - case syscall.SIGINT: - log.Log(log.DEBUG, - "caught SIGINT", - "jobctl") + <- sigChan + log.Log(log.DEBUG, + "caught SIGINT", + "jobctl") + + if CurCancel != nil { + (*CurCancel)() } } } @@ -78,7 +84,7 @@ func waitHandler() { exit := 0 if w != nil { log.Log(log.ERR, - "Child returned error: " + w.Error(), + "Child returned: " + w.Error(), "jobctl") // something outrageous @@ -90,10 +96,6 @@ func waitHandler() { LastExitCode = exit } - - log.Log(log.DEBUG, - "handled sigchld!", - "jobctl") } } @@ -105,7 +107,7 @@ func tcsetpgrp(fd int, pgrp int) error { // wrapper for convenience func setSigState() { signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN, syscall.SIGTSTP) - signal.Notify(sigChan, syscall.SIGINT) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGQUIT) } /* Sets pgid @@ -189,20 +191,24 @@ func LaunchProcess( cmd.Stderr = stderr cmd.Start() - pid := cmd.Process.Pid + cpid := cmd.Process.Pid // TODO: check for clobber? // unlikely, but PIDs do reset after a while - JobMap[pid] = Proc{ Ctl: cmd, Cancel: cancel } + JobMap[cpid] = Proc{ Ctl: cmd, Cancel: cancel } + CurCancel = &cancel if background { go func(){ waitChan <- cmd.Wait() - tcsetpgrp(0, pgid) + delete(JobMap, cpid) + CurCancel = nil }() } else { cmd.Wait() tcsetpgrp(0, pgid) + delete(JobMap, cpid) + CurCancel = nil } } From 91498926d5cf57ed405e4a8a6c596c989add5ab3 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 22 Jul 2020 23:00:25 -0700 Subject: [PATCH 6/9] change liner source so that we can have good tty config --- cmd/shs.go | 8 +++++++- go.mod | 1 + go.sum | 2 ++ stdlib/shell.go | 6 ++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/shs.go b/cmd/shs.go index 739b917..6ac73dd 100644 --- a/cmd/shs.go +++ b/cmd/shs.go @@ -21,7 +21,7 @@ import ( "os" "fmt" "strconv" - "github.com/peterh/liner" + "github.com/candid82/liner" "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/util" @@ -109,6 +109,12 @@ func main() { line := liner.NewLiner() defer line.Close() + if !liner.TerminalSupported() { + log.Log(log.ERR, + "Terminal unsupported, continuing in dummy mode!", + "init") + } + line.SetCtrlCAborts(true) line.SetCompleter(func(line string) (c []string) { return util.ShellCompleter(line, vars, funcs) diff --git a/go.mod b/go.mod index 63a9008..7b6cc51 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.com/whom/shs go 1.14 require ( + github.com/candid82/liner v1.4.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/peterh/liner v1.2.0 // indirect golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 diff --git a/go.sum b/go.sum index ee825b8..dbd0289 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/candid82/liner v1.4.0 h1:nUhs4pv/cnpnBERwJHmqmgargZTWnPbDJ67HtQcfSTo= +github.com/candid82/liner v1.4.0/go.mod h1:shD5EWTOYasmaGjMfuaB82N9YxGMIAEoXjQEH6RoGvo= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= diff --git a/stdlib/shell.go b/stdlib/shell.go index ba12cf5..68afea0 100644 --- a/stdlib/shell.go +++ b/stdlib/shell.go @@ -91,7 +91,7 @@ func waitHandler() { exit = -1024 var e *exec.ExitError if errors.As(w, &e) { - exit = e.Pid() + exit = e.ExitCode() } LastExitCode = exit @@ -156,7 +156,9 @@ func InitShellFeatures() bool { func TeardownShell() { close(sigChan) close(waitChan) - // TODO: Exit all processes in the JobMap + for k := range JobMap { + JobMap[k].Cancel() + } } /* Makes and stores a new process in the job control From 1f192607b2a3f460abf5c5ed31067451727c9e64 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 23 Jul 2020 12:08:29 -0700 Subject: [PATCH 7/9] update debug log usage and add fanciness to log printing --- ast/func_table.go | 15 +++++++++++---- config/config.go | 2 +- log/logger.go | 2 +- stdlib/control_flow.go | 4 ++-- stdlib/list.go | 2 +- stdlib/shell.go | 23 +++++++++-------------- stdlib/stdlib.go | 2 +- util/shell_complete.go | 2 +- 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/ast/func_table.go b/ast/func_table.go index b939e0c..734f9ee 100644 --- a/ast/func_table.go +++ b/ast/func_table.go @@ -17,7 +17,10 @@ package ast -import "gitlab.com/whom/shs/log" +import ( + "fmt" + "gitlab.com/whom/shs/log" +) /* expected function header for any stdlib function */ @@ -61,8 +64,12 @@ func (f Function) ParseFunction(args *Token) bool { if i != 0 { log.Log(log.ERR, - "Incorrect number of arguments", - "eval") + "Incorrect number of arguments", + "eval") + log.Log(log.DEBUG, + fmt.Sprintf("Function %s expects %d arguments. You've provided %d arguments.", + f.Name, f.Args, f.Args - i), + "eval") return false } @@ -89,7 +96,7 @@ func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token { func GetFunction(arg string, table FuncTable) *Function { target, ok := (*table)[arg] if !ok { - log.Log(log.DEBUG, + log.Log(log.INFO, "function " + arg + " not found", "ftable") return nil diff --git a/config/config.go b/config/config.go index fc8eda3..feef48e 100644 --- a/config/config.go +++ b/config/config.go @@ -39,7 +39,7 @@ func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) { util.LoadScript(configFile, vars, funcs) - log.Log(log.DEBUG, + log.Log(log.INFO, "config file fully evaluated", "config") return vars, funcs diff --git a/log/logger.go b/log/logger.go index 2d736e6..7c9a110 100644 --- a/log/logger.go +++ b/log/logger.go @@ -51,5 +51,5 @@ func Log(lvl int, msg, context string) { return } - fmt.Println("[" + context + "] " + msg) + fmt.Println("\033[3m[" + context + "] " + msg + "\033[0m") } diff --git a/stdlib/control_flow.go b/stdlib/control_flow.go index 0238ff5..51c4deb 100644 --- a/stdlib/control_flow.go +++ b/stdlib/control_flow.go @@ -45,8 +45,8 @@ func ShsProgn(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { * (if (eq (number "3") 3) (print "test passed") (print "test failed")) */ func ShsIf(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - cond := in - t := cond.Next + cond := in.Copy() + t := cond.Next.Copy() f := t.Next cond.Next = nil t.Next = nil diff --git a/stdlib/list.go b/stdlib/list.go index cec7d80..748414b 100644 --- a/stdlib/list.go +++ b/stdlib/list.go @@ -29,7 +29,7 @@ import ( */ func Expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { if input.Tag != ast.LIST { - log.Log(log.DEBUG, "expand called on not a list", "expand") + log.Log(log.INFO, "expand called on not a list", "expand") return input } diff --git a/stdlib/shell.go b/stdlib/shell.go index 68afea0..41914ee 100644 --- a/stdlib/shell.go +++ b/stdlib/shell.go @@ -68,7 +68,7 @@ var LastExitCode int func signalHandler() { for { <- sigChan - log.Log(log.DEBUG, + log.Log(log.INFO, "caught SIGINT", "jobctl") @@ -174,7 +174,11 @@ func LaunchProcess( stderr io.Writer ) { c, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(c, path, args...) - cmd.SysProcAttr = &syscall.SysProcAttr{Foreground: !background, Setpgid: true} + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + if !background { + cmd.SysProcAttr.Foreground = true + } if stdin == nil { stdin = os.Stdin @@ -201,6 +205,7 @@ func LaunchProcess( CurCancel = &cancel if background { + cmd.Process.Signal(syscall.SIGTSTP) go func(){ waitChan <- cmd.Wait() delete(JobMap, cpid) @@ -317,7 +322,7 @@ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } ipid := int(pid) - proc, ok := JobMap[ipid] + _, ok := JobMap[ipid] if !ok { log.Log(log.ERR, "Process not found, was it started by this shell?", @@ -325,17 +330,7 @@ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - cmd := proc.Ctl - cmd.Process.Signal(syscall.SIGCONT) - err = cmd.Wait() - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned error: " + err.Error(), "call") - } - } - + tcsetpgrp(0, ipid) return nil } diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 83749c2..292b7da 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -238,7 +238,7 @@ func GenFuncTable() ast.FuncTable { Function: Fg, Name: "foreground", TimesCalled: 0, - Args: 0, + Args: 1, }, "$": &ast.Function{ diff --git a/util/shell_complete.go b/util/shell_complete.go index 2d41440..86410d8 100644 --- a/util/shell_complete.go +++ b/util/shell_complete.go @@ -51,7 +51,7 @@ func ShellCompleter(line string, vt ast.VarTable, ft ast.FuncTable) []string { fobjs, err := ioutil.ReadDir(dir) if err != nil { - log.Log(log.DEBUG, + log.Log(log.INFO, "couldnt read dir " + dir + ": " + err.Error(), "complete") if path { From f34c9070c812ae1fa5cb8040001c1ca5429e9072 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 23 Jul 2020 12:12:56 -0700 Subject: [PATCH 8/9] I decided I wanted it like this. Soon it should be configurable --- log/logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/logger.go b/log/logger.go index 7c9a110..948109d 100644 --- a/log/logger.go +++ b/log/logger.go @@ -51,5 +51,5 @@ func Log(lvl int, msg, context string) { return } - fmt.Println("\033[3m[" + context + "] " + msg + "\033[0m") + fmt.Println("[" + context + "]\033[3m " + msg + "\033[0m") } From 5d538d4a0bafc67389d0f30aff83a6d1e8fe9055 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 23 Jul 2020 19:54:36 -0700 Subject: [PATCH 9/9] fixed background process execution --- cmd/shs.go | 2 ++ log/logger.go | 2 +- stdlib/shell.go | 74 ++++++++++++++++++++++++++----------------------- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/cmd/shs.go b/cmd/shs.go index 6ac73dd..82fbf8d 100644 --- a/cmd/shs.go +++ b/cmd/shs.go @@ -136,6 +136,8 @@ func main() { for { setLogLvl(vars) + stdlib.CheckBGProcs() + var prePrompt string if dyn_prompt != nil { p_tok := dyn_prompt.CallFunction(nil, vars, funcs) diff --git a/log/logger.go b/log/logger.go index 948109d..19732e9 100644 --- a/log/logger.go +++ b/log/logger.go @@ -51,5 +51,5 @@ func Log(lvl int, msg, context string) { return } - fmt.Println("[" + context + "]\033[3m " + msg + "\033[0m") + fmt.Println("[" + context + "]\033[3m " + msg + "\033[0m ") } diff --git a/stdlib/shell.go b/stdlib/shell.go index 41914ee..e67a4bd 100644 --- a/stdlib/shell.go +++ b/stdlib/shell.go @@ -22,7 +22,6 @@ import ( "io" "fmt" "bytes" - "errors" "os/exec" "context" "syscall" @@ -68,7 +67,7 @@ var LastExitCode int func signalHandler() { for { <- sigChan - log.Log(log.INFO, + log.Log(log.DEBUG, "caught SIGINT", "jobctl") @@ -78,27 +77,6 @@ func signalHandler() { } } -func waitHandler() { - for { - w := <- waitChan - exit := 0 - if w != nil { - log.Log(log.ERR, - "Child returned: " + w.Error(), - "jobctl") - - // something outrageous - exit = -1024 - var e *exec.ExitError - if errors.As(w, &e) { - exit = e.ExitCode() - } - - LastExitCode = exit - } - } -} - // for some reason not implemented in stdlib func tcsetpgrp(fd int, pgrp int) error { return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pgrp) @@ -120,7 +98,6 @@ func InitShellFeatures() bool { waitChan = make(chan error, 5) go signalHandler() - go waitHandler() setSigState() pid = os.Getpid() @@ -161,6 +138,35 @@ func TeardownShell() { } } +/* Check background processes states + * If any have finished, reap them + */ +func CheckBGProcs() { + for key, proc := range JobMap { + maybeFilePath := fmt.Sprintf("/proc/%d", key) + _, err := os.Stat(maybeFilePath) + + if os.IsNotExist(err) { + if err := proc.Ctl.Process.Release(); err != nil { + log.Log(log.ERR, + fmt.Sprintf("Failed to release exited process %d: %s", + key, err.Error()), + "jobctl") + } else { + // not actually an error, just abusing default visibility + log.Log(log.ERR, + fmt.Sprintf("Child [%d] exited", key), + "jobctl") + delete(JobMap, key) + } + } else if err != nil { + log.Log(log.DEBUG, + "Error checking if process exists in proc: " + err.Error(), + "jobctl") + } + } +} + /* Makes and stores a new process in the job control * uses os/exec Cmd object. sets SysProcAttr.Foreground * calls Cmd.Start() @@ -204,14 +210,7 @@ func LaunchProcess( JobMap[cpid] = Proc{ Ctl: cmd, Cancel: cancel } CurCancel = &cancel - if background { - cmd.Process.Signal(syscall.SIGTSTP) - go func(){ - waitChan <- cmd.Wait() - delete(JobMap, cpid) - CurCancel = nil - }() - } else { + if !background { cmd.Wait() tcsetpgrp(0, pgid) delete(JobMap, cpid) @@ -322,7 +321,7 @@ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } ipid := int(pid) - _, ok := JobMap[ipid] + proc, ok := JobMap[ipid] if !ok { log.Log(log.ERR, "Process not found, was it started by this shell?", @@ -330,7 +329,14 @@ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - tcsetpgrp(0, ipid) + if err := tcsetpgrp(0, ipid); err != nil { + log.Log(log.ERR, + "Error foregrounding process: " + err.Error(), + "fg") + return nil + } + + proc.Ctl.Wait() return nil }