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 }