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 0000000..3b77b37 Binary files /dev/null and b/resources/icon/icon.kra differ 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 }