github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/lang/process.go (about)

     1  package lang
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/lmorg/murex/app"
    12  	"github.com/lmorg/murex/builtins/pipes/streams"
    13  	"github.com/lmorg/murex/debug"
    14  	"github.com/lmorg/murex/lang/pipes"
    15  	"github.com/lmorg/murex/lang/state"
    16  	"github.com/lmorg/murex/lang/types"
    17  	"github.com/lmorg/murex/utils"
    18  	"github.com/lmorg/murex/utils/ansititle"
    19  )
    20  
    21  var (
    22  	// Interactive describes whether murex is running as an interactive shell or not
    23  	Interactive bool
    24  
    25  	// ShellProcess is the root murex process
    26  	ShellProcess = &Process{}
    27  
    28  	// MxFunctions is a table of global murex functions
    29  	MxFunctions = NewMurexFuncs()
    30  
    31  	// PrivateFunctions is a table of private murex functions
    32  	PrivateFunctions = NewMurexPrivs()
    33  
    34  	// GoFunctions is a table of available builtin functions
    35  	GoFunctions = make(map[string]func(*Process) error)
    36  
    37  	// MethodStdin is a table of all the different commands that can be used as methods
    38  	MethodStdin = newMethods()
    39  
    40  	// MethodStdout is a table of all the different output formats supported by a given command (by default)
    41  	MethodStdout = newMethods()
    42  
    43  	// GlobalVariables is a table of global variables
    44  	GlobalVariables = NewGlobals()
    45  
    46  	// ModuleVariables is a table of module specific variables
    47  	ModuleVariables = NewModuleVars()
    48  
    49  	// GlobalAliases is a table of global aliases
    50  	GlobalAliases = NewAliases()
    51  
    52  	// GlobalPipes is a table of  named pipes
    53  	GlobalPipes = pipes.NewNamed()
    54  
    55  	// GlobalFIDs is a table of running murex processes
    56  	GlobalFIDs = *newFuncID()
    57  
    58  	// GlobalUnitTests is a class for all things murex unit tests
    59  	GlobalUnitTests = new(UnitTests)
    60  
    61  	// ForegroundProc is the murex FID which currently has "focus"
    62  	ForegroundProc = newForegroundProc()
    63  
    64  	// ShellExitNum is for when running murex in interactive shell mode
    65  	ShellExitNum int
    66  )
    67  
    68  func DefineFunction(name string, fn func(*Process) error, StdoutDataType string) {
    69  	GoFunctions[name] = fn
    70  	MethodStdout.Define(name, StdoutDataType)
    71  }
    72  
    73  func DefineMethod(name string, fn func(*Process) error, StdinDataType, StdoutDataType string) {
    74  	GoFunctions[name] = fn
    75  	MethodStdin.Define(name, StdinDataType)
    76  	MethodStdout.Define(name, StdoutDataType)
    77  }
    78  
    79  func indentError(err error) error {
    80  	if err == nil {
    81  		return err
    82  	}
    83  
    84  	s := strings.ReplaceAll(err.Error(), "\n", "\n    ")
    85  	return errors.New(s)
    86  }
    87  
    88  func writeError(p *Process, err error) []byte {
    89  	var msg string
    90  
    91  	name := p.Name.String()
    92  	if name == "exec" {
    93  		exec, pErr := p.Parameters.String(0)
    94  		if pErr == nil {
    95  			name = exec
    96  		}
    97  	}
    98  
    99  	var indentLen int
   100  	if p.FileRef.Source.Module == app.ShellModule {
   101  		msg = fmt.Sprintf("Error in `%s` (%d,%d): ", name, p.FileRef.Line, p.FileRef.Column)
   102  		indentLen = len(msg) - 2
   103  	} else {
   104  		msg = fmt.Sprintf("Error in `%s` (%s %d,%d):\n      Command: %s\n      Error: ", name, p.FileRef.Source.Filename, p.FileRef.Line+1, p.FileRef.Column, string(p.raw))
   105  		indentLen = 6 + 5
   106  	}
   107  
   108  	sErr := strings.ReplaceAll(err.Error(), utils.NewLineString, utils.NewLineString+strings.Repeat(" ", indentLen)+"> ")
   109  	return []byte(msg + sErr)
   110  }
   111  
   112  func createProcess(p *Process, isMethod bool) {
   113  	GlobalFIDs.Register(p) // This also registers the variables process
   114  	p.CreationTime = time.Now()
   115  
   116  	parseRedirection(p)
   117  
   118  	name := p.Name.String()
   119  
   120  	if name[0] == '!' {
   121  		p.IsNot = true
   122  	}
   123  
   124  	p.IsMethod = isMethod
   125  
   126  	// We do stderr first so we can log errors in the stdout pipe to stderr
   127  	switch p.NamedPipeErr {
   128  	case "":
   129  		p.NamedPipeErr = "err"
   130  	case "err":
   131  		//p.Stderr.Writeln([]byte("Invalid usage of named pipes: stderr defaults to <err>."))
   132  	case "out":
   133  		p.Stderr = p.Next.Stdin
   134  	default:
   135  		pipe, err := GlobalPipes.Get(p.NamedPipeErr)
   136  		if err == nil {
   137  			p.Stderr = pipe
   138  		} else {
   139  			p.Stderr.Writeln([]byte("invalid usage of named pipes: " + err.Error()))
   140  		}
   141  	}
   142  
   143  	// We do stdout last so we can log errors in the stdout pipe to stderr
   144  	switch p.NamedPipeOut {
   145  	case "":
   146  		p.NamedPipeOut = "out"
   147  	case "err":
   148  		p.Stdout.SetDataType(types.Generic)
   149  		p.Stdout = p.Next.Stderr
   150  	case "out":
   151  		//p.Stderr.Writeln([]byte("Invalid usage of named pipes: stdout defaults to <out>."))
   152  	default:
   153  		pipe, err := GlobalPipes.Get(p.NamedPipeOut)
   154  		if err == nil {
   155  			p.stdoutOldPtr = p.Stdout
   156  			p.Stdout = pipe
   157  		} else {
   158  			p.Stderr.Writeln([]byte("invalid usage of named pipes: " + err.Error()))
   159  		}
   160  	}
   161  
   162  	// Test cases
   163  	if p.NamedPipeTest != "" {
   164  		var stdout2, stderr2 *streams.Stdin
   165  		p.Stdout, stdout2 = streams.NewTee(p.Stdout)
   166  		p.Stderr, stderr2 = streams.NewTee(p.Stderr)
   167  		err := p.Tests.SetStreams(p.NamedPipeTest, stdout2, stderr2, &p.ExitNum)
   168  		if err != nil {
   169  			p.Stderr.Writeln([]byte("invalid usage of named pipes: " + err.Error()))
   170  		}
   171  	}
   172  
   173  	ttys(p)
   174  
   175  	p.Stdout.Open()
   176  	p.Stderr.Open()
   177  
   178  	p.State.Set(state.Assigned)
   179  
   180  	// Lets run `pipe` and `test` ahead of time to fudge the use of named pipes
   181  	if name == "pipe" || (name == "test" && len(p.Parameters.PreParsed) > 0 &&
   182  		(string(p.Parameters.PreParsed[0]) == "config" || // test config
   183  			string(p.Parameters.PreParsed[0]) == "define" || // test define
   184  			string(p.Parameters.PreParsed[0]) == "state")) { // test state
   185  
   186  		_, params, err := ParseStatementParameters(p.raw, p)
   187  		if err != nil {
   188  			ShellProcess.Stderr.Writeln(writeError(p, err))
   189  			if p.ExitNum == 0 {
   190  				p.ExitNum = 1
   191  			}
   192  
   193  		} else {
   194  			p.Parameters.DefineParsed(params)
   195  			err = GoFunctions[name[:4]](p)
   196  			if err != nil {
   197  				ShellProcess.Stderr.Writeln(writeError(p, err))
   198  				if p.ExitNum == 0 {
   199  					p.ExitNum = 1
   200  				}
   201  			}
   202  		}
   203  
   204  		p.SetTerminatedState(true)
   205  		p.State.Set(state.Executed)
   206  	}
   207  }
   208  
   209  func executeProcess(p *Process) {
   210  	testStates(p)
   211  
   212  	if p.HasTerminated() || p.HasCancelled() ||
   213  		/*p.Parent.HasTerminated() ||*/ p.Parent.HasCancelled() {
   214  		destroyProcess(p)
   215  		return
   216  	}
   217  
   218  	p.State.Set(state.Starting)
   219  
   220  	var err error
   221  	name := p.Name.String()
   222  	echo, err := p.Config.Get("proc", "echo", types.Boolean)
   223  	if err != nil {
   224  		echo = false
   225  	}
   226  
   227  	tmux, err := p.Config.Get("proc", "echo-tmux", types.Boolean)
   228  	if err != nil {
   229  		tmux = false
   230  	}
   231  
   232  	var parsedAlias bool
   233  
   234  	n, params, err := ParseStatementParameters(p.raw, p)
   235  	if err != nil {
   236  		goto cleanUpProcess
   237  	}
   238  	if n != name {
   239  		p.Name.Set(n)
   240  		name = n
   241  	}
   242  	p.Parameters.DefineParsed(params)
   243  
   244  	// Execute function
   245  	p.State.Set(state.Executing)
   246  	p.StartTime = time.Now()
   247  
   248  	if err := GlobalFIDs.Executing(p.Id); err != nil {
   249  		panic(err)
   250  	}
   251  
   252  	if p.cache != nil && p.cache.use {
   253  		// we have a preview cache, lets just write that and skip execution
   254  		_, err = p.Stdout.Write(p.cache.b.stdout)
   255  		//panic(fmt.Sprintf("%v: '%s'", previewCache.raw, string(p.cache.b.stdout)))
   256  		p.Stdout.SetDataType(p.cache.dt.stdout)
   257  		if err != nil {
   258  			panic(err)
   259  		}
   260  		_, _ = p.Stderr.Write(p.cache.b.stderr)
   261  
   262  		p.Stderr.SetDataType(p.cache.dt.stderr)
   263  		if err != nil {
   264  			panic(err)
   265  		}
   266  
   267  		goto cleanUpProcess
   268  	}
   269  
   270  executeProcess:
   271  	if !p.Background.Get() || debug.Enabled {
   272  		if echo.(bool) {
   273  			params := strings.Replace(strings.Join(p.Parameters.StringArray(), `", "`), "\n", "\n# ", -1)
   274  			os.Stdout.WriteString("# " + name + `("` + params + `");` + utils.NewLineString)
   275  		}
   276  
   277  		if tmux.(bool) {
   278  			ansititle.Tmux([]byte(name))
   279  		}
   280  
   281  		//ansititle.Write([]byte(name))
   282  	}
   283  
   284  	// execution mode:
   285  	switch {
   286  	case p.Scope.Id != ShellProcess.Id && PrivateFunctions.Exists(name, p.FileRef):
   287  		// murex privates
   288  		fn := PrivateFunctions.get(name, p.FileRef)
   289  		if fn != nil {
   290  			fork := p.Fork(F_FUNCTION)
   291  			fork.Name.Set(name)
   292  			fork.Parameters.CopyFrom(&p.Parameters)
   293  			fork.FileRef = fn.FileRef
   294  			p.ExitNum, err = fork.Execute(fn.Block)
   295  		}
   296  
   297  	case GlobalAliases.Exists(name) && p.Parent.Name.String() != "alias" && !parsedAlias:
   298  		// murex aliases
   299  		alias := GlobalAliases.Get(name)
   300  		p.Name.Set(alias[0])
   301  		name = alias[0]
   302  		p.Parameters.Prepend(alias[1:])
   303  		parsedAlias = true
   304  		goto executeProcess
   305  
   306  	case MxFunctions.Exists(name):
   307  		// murex functions
   308  		fn := MxFunctions.get(name)
   309  		if fn != nil {
   310  			fork := p.Fork(F_FUNCTION)
   311  			fork.Name.Set(name)
   312  			fork.Parameters.CopyFrom(&p.Parameters)
   313  			fork.FileRef = fn.FileRef
   314  			err = fn.castParameters(fork.Process)
   315  			if err == nil {
   316  				p.ExitNum, err = fork.Execute(fn.Block)
   317  			}
   318  		}
   319  
   320  	case GoFunctions[name] != nil:
   321  		// murex builtins
   322  		err = GoFunctions[name](p)
   323  
   324  	default:
   325  		if p.Parameters.Len() == 0 {
   326  			v, _ := ShellProcess.Config.Get("shell", "auto-cd", types.Boolean)
   327  			autoCd, _ := v.(bool)
   328  			if autoCd {
   329  				fileInfo, _ := os.Stat(name)
   330  				if fileInfo != nil && fileInfo.IsDir() {
   331  					p.Parameters.Prepend([]string{name})
   332  					name = "cd"
   333  					goto executeProcess
   334  				}
   335  			}
   336  		}
   337  
   338  		// shell execute
   339  		p.Parameters.Prepend([]string{name})
   340  		p.Name.Set("exec")
   341  		err = GoFunctions["exec"](p)
   342  		if err != nil && strings.Contains(err.Error(), "executable file not found") {
   343  			_, cpErr := ParseExpression(p.raw, 0, false)
   344  			err = fmt.Errorf("Not a valid expression:\n    %v\nNor a valid statement:\n    %v",
   345  				indentError(cpErr), indentError(err))
   346  		}
   347  	}
   348  
   349  	if p.cache != nil {
   350  		p.cache.b.stdout, _ = p.cache.tee.stdout.ReadAll()
   351  		p.cache.dt.stdout = p.cache.tee.stdout.GetDataType()
   352  		p.cache.b.stderr, _ = p.cache.tee.stderr.ReadAll()
   353  		p.cache.dt.stderr = p.cache.tee.stderr.GetDataType()
   354  	}
   355  
   356  cleanUpProcess:
   357  	//debug.Json("Execute process (cleanUpProcess)", p)
   358  
   359  	if err != nil {
   360  		p.Stderr.Writeln(writeError(p, err))
   361  		if p.ExitNum == 0 {
   362  			p.ExitNum = 1
   363  		}
   364  	}
   365  
   366  	if p.CCEvent != nil {
   367  		p.CCEvent(name, p)
   368  	}
   369  
   370  	p.State.Set(state.Executed)
   371  
   372  	if p.NamedPipeTest != "" {
   373  		testEnabled, err := p.Config.Get("test", "enabled", types.Boolean)
   374  		if err == nil && testEnabled.(bool) {
   375  			p.Tests.Compare(p.NamedPipeTest, p)
   376  		}
   377  	}
   378  
   379  	if len(p.NamedPipeOut) > 7 /* tmp:$FID/$MD5 */ && p.NamedPipeOut[:4] == "tmp:" {
   380  		out, err := GlobalPipes.Get(p.NamedPipeOut)
   381  		if err != nil {
   382  			p.Stderr.Writeln([]byte(fmt.Sprintf("Error connecting to temporary named pipe '%s': %s", p.NamedPipeOut, err.Error())))
   383  		} else {
   384  			p.stdoutOldPtr.Open()
   385  			_, err = out.WriteTo(p.stdoutOldPtr)
   386  			p.stdoutOldPtr.Close()
   387  			if err != nil && err != io.EOF {
   388  				p.Stderr.Writeln([]byte(fmt.Sprintf("Error piping from temporary named pipe '%s': %s", p.NamedPipeOut, err.Error())))
   389  			}
   390  		}
   391  
   392  		err = GlobalPipes.Close(p.NamedPipeOut)
   393  		if err != nil {
   394  			p.Stderr.Writeln([]byte(fmt.Sprintf("Error closing temporary named pipe '%s': %s", p.NamedPipeOut, err.Error())))
   395  		}
   396  	}
   397  
   398  	for !p.Previous.HasTerminated() {
   399  		// Code shouldn't really get stuck here.
   400  		// This would only happen if someone abuses pipes on a function that has no stdin.
   401  		time.Sleep(10 * time.Microsecond)
   402  	}
   403  
   404  	//debug.Json("Execute process (destroyProcess)", p)
   405  	destroyProcess(p)
   406  }
   407  
   408  func waitProcess(p *Process) {
   409  	//debug.Log("Waiting for", p.Name.String())
   410  	<-p.WaitForTermination
   411  	//debug.Log("Finished waiting for", p.Name.String())
   412  }
   413  
   414  func destroyProcess(p *Process) {
   415  	//debug.Json("destroyProcess ()", p)
   416  	// Clean up any context goroutines
   417  	go p.Done()
   418  
   419  	// Make special case for `bg` because that doesn't wait.
   420  	if p.Name.String() != "bg" {
   421  		//debug.Json("destroyProcess (p.WaitForTermination <- false)", p)
   422  		p.WaitForTermination <- false
   423  	}
   424  
   425  	//debug.Json("destroyProcess (deregisterProcess)", p)
   426  	deregisterProcess(p)
   427  	//debug.Json("destroyProcess (end)", p)
   428  }
   429  
   430  // deregisterProcess deregisters a murex process, FID and mark variables for
   431  // garbage collection.
   432  func deregisterProcess(p *Process) {
   433  	//debug.Json("deregisterProcess ()", p)
   434  
   435  	p.State.Set(state.Terminating)
   436  
   437  	p.Stdout.Close()
   438  	p.Stderr.Close()
   439  
   440  	p.SetTerminatedState(true)
   441  	if !p.Background.Get() {
   442  		ForegroundProc.Set(p.Next)
   443  	}
   444  
   445  	go func() {
   446  		p.State.Set(state.AwaitingGC)
   447  		GlobalFIDs.Deregister(p.Id)
   448  	}()
   449  }