/* 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 stdlib import ( "os" "errors" "os/exec" "context" "syscall" "golang.org/x/sys/unix" "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 /* 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 * * Example (l vim file.txt) */ func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in == nil { return nil } if in.Tag == ast.LIST { log.Log(log.ERR, "couldnt exec, target bin is a list", "call") return nil } path, err := exec.LookPath(in.Value()) if err != nil { log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") return nil } args := []string{} for i := in.Next; i != nil; i = i.Next { if i.Tag == ast.LIST { log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") return nil } args = append(args, i.Value()) } LaunchProcess(path, args, false) return nil } /* Starts a call in the background * Takes n args (a shell command not delimited by string delimiters) * * Example: (bg vim file.txt) */ func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if in == nil { return nil } if in.Tag == ast.LIST { log.Log(log.ERR, "couldnt exec, target bin is a list", "call") return nil } path, err := exec.LookPath(in.Value()) if err != nil { log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") return nil } args := []string{} for i := in.Next; i != nil; i = i.Next { if i.Tag == ast.LIST { log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") return nil } args = append(args, i.Value()) } LaunchProcess(path, args, true) return nil } /* brings last BG'ed process into the foreground * returns nil * * Example: * (bg vim file.txt) * (fg) */ func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if len(bgProcs) < 1 { return nil } cmd := bgProcs[0] bgProcs = bgProcs[1:] 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...) 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") } } 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 * * Example: * (bg ping google.com) * (bg .........) * (jobs) */ func Jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { ret := &ast.Token{ Tag: ast.LIST, } _inner := &ast.Token{ Tag: ast.STRING, } ret.Direct(_inner) _inner.Set(fmt.Sprintf("Total: %d", len(bgProcs))) iter := &_inner for i := 0; i < len(bgProcs); i += 1 { (*iter).Next = &ast.Token{ Tag: ast.STRING, } (*iter).Next.Set(fmt.Sprintf("[%d]: %d", i, bgProcs[i].Process.Pid)) iter = &(*iter).Next } return ret } /* calls a command (blocks until completion) * captures stdout and returns it as a string * * Example: ($ echo hello world) */ func ReadCmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in == nil { return nil } if in.Tag == ast.LIST { log.Log(log.ERR, "couldnt exec, target bin is a list", "call") return nil } var out bytes.Buffer path, err := exec.LookPath(in.Value()) if err != nil { log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") return nil } args := []string{} for i := in.Next; i != nil; i = i.Next { if i.Tag == ast.LIST { log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") return nil } 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(), "$") } } output := out.String() olen := len(output) if olen > 0 && output[olen - 1] == '\n' { output = output[:olen - 1] } ret := &ast.Token{Tag: ast.STRING} ret.Set(output) return ret } /* Takes 0 arguments * returns the exit code of the last executed program * * Example: * (sudo apt update) * (?) <- gets exit code */ func GetExit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { ret := &ast.Token{Tag: ast.NUMBER} ret.Set(fmt.Sprintf("%d", LastExitCode)) return ret } /* takes an argument (pid of process to be killed) * calls Process.Kill() on it * do not use this if you already have a native implementation * (this function not added to functable in stdlib.go) */ func Kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in.Tag == ast.LIST { log.Log(log.ERR, "non-number argument to kill function", "kill") return nil } pid, err := strconv.ParseInt(in.Value(), 10, 64) if err != nil { log.Log(log.ERR, "error parsing arg to kill: " + err.Error(), "kill") 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") } } return nil }