Merge branch 'dev' into 'master'

Proper Job Control

Closes #9 and #1

See merge request whom/shs!9
This commit is contained in:
Aidan Hahn 2020-07-24 02:59:08 +00:00
commit ed20c03363
14 changed files with 332 additions and 158 deletions

View file

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

View file

@ -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
*/
@ -63,6 +66,10 @@ func (f Function) ParseFunction(args *Token) bool {
log.Log(log.ERR,
"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

View file

@ -21,10 +21,11 @@ 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"
"gitlab.com/whom/shs/stdlib"
"gitlab.com/whom/shs/config"
)
@ -77,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 {
@ -105,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)
@ -126,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)

36
cmd/test_shell.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}

View file

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

2
go.mod
View file

@ -3,6 +3,8 @@ 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
)

4
go.sum
View file

@ -1,6 +1,10 @@
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=
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=

View file

@ -51,5 +51,5 @@ func Log(lvl int, msg, context string) {
return
}
fmt.Println("[" + context + "] " + msg)
fmt.Println("[" + context + "]\033[3m " + msg + "\033[0m ")
}

BIN
resources/icon/icon.kra Normal file

Binary file not shown.

View file

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

View file

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

View file

@ -19,30 +19,205 @@ package stdlib
import (
"os"
"io"
"fmt"
"bytes"
"strconv"
"os/exec"
"context"
"syscall"
"strconv"
"os/signal"
"golang.org/x/sys/unix"
"gitlab.com/whom/shs/ast"
"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,
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, 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 {
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 {
<- sigChan
log.Log(log.DEBUG,
"caught SIGINT",
"jobctl")
if CurCancel != nil {
(*CurCancel)()
}
}
}
// 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, syscall.SIGQUIT)
}
/* Sets pgid
* Installs a sigchld handler
* returns true if success, false on error
*/
func InitShellFeatures() bool {
// TODO: adjust capacity, make configurable maybe?
sigChan = make(chan os.Signal, 5)
waitChan = make(chan error, 5)
go signalHandler()
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
}
/* Tears down session
*/
func TeardownShell() {
close(sigChan)
close(waitChan)
for k := range JobMap {
JobMap[k].Cancel()
}
}
/* 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()
*/
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{Setpgid: true}
if !background {
cmd.SysProcAttr.Foreground = true
}
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()
cpid := cmd.Process.Pid
// TODO: check for clobber?
// unlikely, but PIDs do reset after a while
JobMap[cpid] = Proc{ Ctl: cmd, Cancel: cancel }
CurCancel = &cancel
if !background {
cmd.Wait()
tcsetpgrp(0, pgid)
delete(JobMap, cpid)
CurCancel = nil
}
}
/* 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 +251,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, nil, nil, nil)
return nil
}
@ -114,6 +261,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
}
@ -139,64 +287,61 @@ 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, 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)
* ( <call to ps or jobs> )
* (fg <pid>)
*/
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)
}()
cmd.Process.Signal(syscall.SIGCONT)
err := cmd.Wait()
close(signalChan)
signal.Reset(sigs...)
pid, err := strconv.ParseFloat(in.Value(), 64)
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")
}
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
}
if err := tcsetpgrp(0, ipid); err != nil {
log.Log(log.ERR,
"Error foregrounding process: " + err.Error(),
"fg")
return nil
}
proc.Ctl.Wait()
return nil
}
/* 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)
@ -208,22 +353,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
}
@ -244,7 +385,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")
@ -261,34 +402,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' {
@ -333,33 +447,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
}

View file

@ -238,7 +238,7 @@ func GenFuncTable() ast.FuncTable {
Function: Fg,
Name: "foreground",
TimesCalled: 0,
Args: 0,
Args: 1,
},
"$": &ast.Function{

View file

@ -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 {
@ -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)
}
}