github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/lib/iolib/iolib.go (about) 1 package iolib 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "runtime" 11 "syscall" 12 13 "github.com/arnodel/golua/lib/packagelib" 14 rt "github.com/arnodel/golua/runtime" 15 ) 16 17 // BufferedStdFiles sets wether std files should be buffered 18 var BufferedStdFiles bool = true 19 20 type ioKeyType struct{} 21 22 var ioKey = rt.AsValue(ioKeyType{}) 23 24 // LibLoader can load the io lib. 25 var LibLoader = packagelib.Loader{ 26 Load: load, 27 Name: "io", 28 } 29 30 func load(r *rt.Runtime) (rt.Value, func()) { 31 methods := rt.NewTable() 32 33 meta := rt.NewTable() 34 r.SetEnv(meta, "__name", rt.StringValue("file")) 35 r.SetEnv(meta, "__index", rt.TableValue(methods)) 36 37 rt.SolemnlyDeclareCompliance( 38 rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyIoSafe, 39 40 r.SetEnvGoFunc(methods, "read", fileread, 1, true), 41 r.SetEnvGoFunc(methods, "lines", filelines, 1, true), 42 r.SetEnvGoFunc(methods, "close", fileclose, 1, false), 43 r.SetEnvGoFunc(methods, "flush", fileflush, 1, false), 44 r.SetEnvGoFunc(methods, "seek", fileseek, 3, false), 45 r.SetEnvGoFunc(methods, "setvbuf", filesetvbuf, 3, false), 46 // TODO: setvbuf, 47 r.SetEnvGoFunc(methods, "write", filewrite, 1, true), 48 49 r.SetEnvGoFunc(meta, "__close", file__close, 1, false), 50 // r.SetEnvGoFunc(meta, "__gc", file__close, 1, false), 51 ) 52 53 rt.SolemnlyDeclareCompliance( 54 rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyTimeSafe|rt.ComplyTimeSafe|rt.ComplyIoSafe, 55 56 r.SetEnvGoFunc(meta, "__tostring", tostring, 1, false), 57 ) 58 59 var ( 60 stdoutOpts = statusNotClosable 61 stderrOpts = statusNotClosable 62 stdinOpts = statusNotClosable 63 ) 64 if BufferedStdFiles { 65 stdoutOpts |= bufferedWrite 66 stdinOpts |= bufferedRead 67 } 68 69 stdinFile := NewFile(os.Stdin, stdinOpts) 70 stdoutFile := NewFile(os.Stdout, stdoutOpts) 71 stderrFile := NewFile(os.Stderr, stderrOpts) 72 // This is not a good pattern - it has to do for now. 73 if r.Stdout == nil { 74 r.Stdout = stdoutFile.writer 75 } 76 stdin := r.NewUserDataValue(stdinFile, meta) 77 stdout := r.NewUserDataValue(stdoutFile, meta) 78 stderr := r.NewUserDataValue(stderrFile, meta) // I''m guessing, don't buffer stderr? 79 80 r.SetRegistry(ioKey, rt.AsValue(&ioData{ 81 defaultOutput: stdout.AsUserData(), 82 defaultInput: stdin.AsUserData(), 83 metatable: meta, 84 })) 85 pkg := rt.NewTable() 86 r.SetEnv(pkg, "stdin", stdin) 87 r.SetEnv(pkg, "stdout", stdout) 88 r.SetEnv(pkg, "stderr", stderr) 89 90 rt.SolemnlyDeclareCompliance( 91 rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyIoSafe, 92 93 r.SetEnvGoFunc(pkg, "close", ioclose, 1, false), 94 r.SetEnvGoFunc(pkg, "flush", ioflush, 0, false), 95 r.SetEnvGoFunc(pkg, "input", input, 1, false), 96 r.SetEnvGoFunc(pkg, "lines", iolines, 1, true), 97 r.SetEnvGoFunc(pkg, "open", open, 2, false), 98 r.SetEnvGoFunc(pkg, "output", output, 1, false), 99 r.SetEnvGoFunc(pkg, "popen", popen, 2, false), 100 r.SetEnvGoFunc(pkg, "read", ioread, 0, true), 101 r.SetEnvGoFunc(pkg, "tmpfile", tmpfile, 0, false), 102 r.SetEnvGoFunc(pkg, "write", iowrite, 0, true), 103 ) 104 105 rt.SolemnlyDeclareCompliance( 106 rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyTimeSafe|rt.ComplyIoSafe, 107 108 r.SetEnvGoFunc(pkg, "type", typef, 1, false), 109 ) 110 111 // This function should make sure known buffers are flushed before quitting 112 var cleanup = func() { 113 getIoData(r).defaultOutputFile().Flush() 114 stdoutFile.Flush() 115 stderrFile.Flush() 116 } 117 118 return rt.TableValue(pkg), cleanup 119 } 120 121 type ioData struct { 122 defaultOutput *rt.UserData 123 defaultInput *rt.UserData 124 metatable *rt.Table 125 } 126 127 func getIoData(r *rt.Runtime) *ioData { 128 return r.Registry(ioKey).Interface().(*ioData) 129 } 130 131 func (d *ioData) defaultOutputFile() *File { 132 return d.defaultOutput.Value().(*File) 133 } 134 135 func (d *ioData) defaultInputFile() *File { 136 return d.defaultInput.Value().(*File) 137 } 138 139 func pushingNextIoResult(r *rt.Runtime, c *rt.GoCont, ioErr error) (rt.Cont, error) { 140 next := c.Next() 141 if ioErr != nil { 142 return r.ProcessIoError(next, ioErr) 143 } 144 r.Push1(next, rt.BoolValue(true)) 145 return next, nil 146 } 147 148 func ioclose(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 149 var f *File 150 if c.NArgs() == 0 { 151 f = getIoData(t.Runtime).defaultOutputFile() 152 } else { 153 var err error 154 f, err = FileArg(c, 0) 155 if err != nil { 156 return nil, err 157 } 158 } 159 160 var cont rt.Cont 161 var err error 162 if f.file == nil { 163 cont, err = f.close(t, c) 164 } else { 165 cont, err = pushingNextIoResult(t.Runtime, c, f.Close()) 166 } 167 168 return cont, err 169 } 170 171 func fileclose(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 172 if err := c.Check1Arg(); err != nil { 173 return nil, err 174 } 175 return ioclose(t, c) 176 } 177 178 func file__close(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 179 if err := c.Check1Arg(); err != nil { 180 return nil, err 181 } 182 f, err := FileArg(c, 0) 183 if err != nil { 184 return nil, err 185 } 186 f.cleanup() 187 return c.Next(), nil 188 } 189 190 func ioflush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 191 var f *File 192 if c.NArgs() == 0 { 193 f = getIoData(t.Runtime).defaultOutputFile() 194 } else { 195 var err error 196 f, err = FileArg(c, 0) 197 if err != nil { 198 return nil, err 199 } 200 } 201 return pushingNextIoResult(t.Runtime, c, f.Flush()) 202 } 203 204 func fileflush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 205 if err := c.Check1Arg(); err != nil { 206 return nil, err 207 } 208 return ioflush(t, c) 209 } 210 211 func errFileOrFilename() error { 212 return errors.New("#1 must be a file or a filename") 213 } 214 215 func input(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 216 ioData := getIoData(t.Runtime) 217 if c.NArgs() == 0 { 218 return c.PushingNext1(t.Runtime, rt.UserDataValue(ioData.defaultInput)), nil 219 } 220 var ( 221 fv rt.Value 222 arg = c.Arg(0) 223 ) 224 switch arg.Type() { 225 case rt.StringType: 226 f, ioErr := OpenFile(t.Runtime, arg.AsString(), "r") 227 if ioErr != nil { 228 return nil, ioErr 229 } 230 fv = t.NewUserDataValue(f, ioData.metatable) 231 case rt.UserDataType: 232 _, err := FileArg(c, 0) 233 if err != nil { 234 return nil, errFileOrFilename() 235 } 236 fv = arg 237 default: 238 return nil, errFileOrFilename() 239 } 240 ioData.defaultInput = fv.AsUserData() 241 return c.PushingNext1(t.Runtime, fv), nil 242 } 243 244 func output(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 245 ioData := getIoData(t.Runtime) 246 if c.NArgs() == 0 { 247 return c.PushingNext1(t.Runtime, rt.UserDataValue(ioData.defaultOutput)), nil 248 } 249 var ( 250 fv rt.Value 251 arg = c.Arg(0) 252 ) 253 switch arg.Type() { 254 case rt.StringType: 255 f, ioErr := OpenFile(t.Runtime, arg.AsString(), "w") 256 if ioErr != nil { 257 return nil, ioErr 258 } 259 fv = t.NewUserDataValue(f, ioData.metatable) 260 case rt.UserDataType: 261 _, err := FileArg(c, 0) 262 if err != nil { 263 return nil, errFileOrFilename() 264 } 265 fv = arg 266 default: 267 return nil, errFileOrFilename() 268 } 269 // Make sure the current output is flushed 270 ioData.defaultOutput.Value().(*File).Flush() 271 ioData.defaultOutput = fv.AsUserData() 272 return c.PushingNext1(t.Runtime, fv), nil 273 } 274 275 func iolines(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 276 var ( 277 f *File 278 eofAction = closeAtEOF 279 ) 280 if c.NArgs() == 0 || c.Arg(0) == rt.NilValue { 281 f = getIoData(t.Runtime).defaultInputFile() 282 eofAction = doNotCloseAtEOF 283 } else { 284 fname, err := c.StringArg(0) 285 if err != nil { 286 return nil, err 287 } 288 var ioErr error 289 f, ioErr = OpenFile(t.Runtime, string(fname), "r") 290 if ioErr != nil { 291 return nil, ioErr 292 } 293 } 294 readers, fmtErr := getFormatReaders(c.Etc()) 295 if fmtErr != nil { 296 return nil, fmtErr 297 } 298 return c.PushingNext(t.Runtime, rt.FunctionValue(lines(t.Runtime, f, readers, eofAction))), nil 299 } 300 301 func filelines(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 302 if err := c.Check1Arg(); err != nil { 303 return nil, err 304 } 305 f, err := FileArg(c, 0) 306 if err != nil { 307 return nil, err 308 } 309 readers, fmtErr := getFormatReaders(c.Etc()) 310 if fmtErr != nil { 311 return nil, fmtErr 312 } 313 314 return c.PushingNext(t.Runtime, rt.FunctionValue(lines(t.Runtime, f, readers, doNotCloseAtEOF))), nil 315 } 316 317 const ( 318 closeAtEOF = 1 319 doNotCloseAtEOF = 0 320 ) 321 322 func lines(r *rt.Runtime, f *File, readers []formatReader, flags int) *rt.GoFunction { 323 if len(readers) == 0 { 324 readers = []formatReader{lineReader(false)} 325 } 326 iterator := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 327 next := c.Next() 328 // if f.closed { 329 // return next, nil 330 // } 331 err := read(r, f, readers, next) 332 if err != nil { 333 if err == io.EOF { 334 if flags&closeAtEOF != 0 { 335 if err := f.Close(); err != nil { 336 return t.ProcessIoError(next, err) 337 } 338 } 339 t.Push1(next, rt.NilValue) 340 return next, nil 341 } 342 return nil, err 343 } 344 return next, nil 345 } 346 iterGof := rt.NewGoFunction(iterator, "linesiterator", 0, false) 347 iterGof.SolemnlyDeclareCompliance(rt.ComplyCpuSafe | rt.ComplyMemSafe | rt.ComplyIoSafe) 348 return iterGof 349 350 } 351 352 func open(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 353 if err := c.Check1Arg(); err != nil { 354 return nil, err 355 } 356 fname, err := c.StringArg(0) 357 if err != nil { 358 return nil, err 359 } 360 mode := "r" 361 if c.NArgs() >= 2 { 362 mode, err = c.StringArg(1) 363 if err != nil { 364 return nil, err 365 } 366 } 367 f, ioErr := OpenFile(t.Runtime, fname, mode) 368 if ioErr != nil { 369 return pushingNextIoResult(t.Runtime, c, ioErr) 370 } 371 372 fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable) 373 return c.PushingNext(t.Runtime, fv), nil 374 } 375 376 func popen(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 377 if err := c.Check1Arg(); err != nil { 378 return nil, err 379 } 380 cmdStr, err := c.StringArg(0) 381 if err != nil { 382 return nil, err 383 } 384 385 mode := "r" 386 if c.NArgs() >= 2 { 387 mode, err = c.StringArg(1) 388 if err != nil { 389 return nil, err 390 } 391 } 392 393 var cmdArgs []string 394 if runtime.GOOS == "windows" { 395 cmdArgs = []string{"C:\\Windows\\system32\\cmd.exe", "/c", cmdStr} 396 } else { 397 cmdArgs = []string{"/bin/sh", "-c", cmdStr} 398 } 399 400 cmd := exec.Cmd{ 401 Path: cmdArgs[0], 402 Args: cmdArgs, 403 } 404 405 outDummy, inDummy, err := os.Pipe() 406 if err != nil { 407 return nil, err 408 } 409 410 f := &File{ 411 writer: &nobufWriter{inDummy}, 412 reader: &nobufReader{outDummy}, 413 name: cmdStr, 414 } 415 416 var stdout io.ReadCloser 417 var stdin io.WriteCloser 418 switch mode { 419 case "r": 420 stdout, err = cmd.StdoutPipe() 421 f.reader = bufio.NewReader(stdout) 422 case "w": 423 stdin, err = cmd.StdinPipe() 424 f.writer = bufio.NewWriterSize(stdin, 65536) 425 default: 426 err = errors.New("invalid mode") 427 } 428 // called *only* from io.close 429 f.close = func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 430 err := f.Close() 431 if err != nil { 432 return pushingNextIoResult(t.Runtime, c, err) 433 } 434 435 if stdout != nil { 436 err := stdout.Close() 437 if err != nil { 438 return pushingNextIoResult(t.Runtime, c, err) 439 } 440 } 441 if stdin != nil { 442 err := stdin.Close() 443 if err != nil { 444 return pushingNextIoResult(t.Runtime, c, err) 445 } 446 } 447 448 cont := c.Next() 449 450 cmd.Wait() 451 ps := cmd.ProcessState 452 if ps.Success() { 453 t.Runtime.Push(cont, rt.BoolValue(true)) 454 } else { 455 t.Runtime.Push(cont, rt.NilValue) 456 } 457 458 exit := rt.StringValue("exit") 459 code := rt.IntValue(int64(ps.ExitCode())) 460 if !ps.Exited() { 461 // received signal instead of normal exit 462 exit = rt.StringValue("signal") 463 if runtime.GOOS != "windows" { 464 // i can't find out what Sys() is on windows ... 465 ws := ps.Sys().(syscall.WaitStatus) 466 sig := ws.Signal() 467 code = rt.IntValue(int64(sig)) // syscall.Signal 468 } 469 } 470 471 t.Runtime.Push(cont, exit, code) 472 473 return c.Next(), nil 474 } 475 476 if err != nil { 477 return pushingNextIoResult(t.Runtime, c, err) 478 } 479 480 err = cmd.Start() 481 if err != nil { 482 return nil, err 483 } 484 485 fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable) 486 return c.PushingNext(t.Runtime, fv), nil 487 } 488 489 func typef(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 490 if err := c.Check1Arg(); err != nil { 491 return nil, err 492 } 493 f, err := FileArg(c, 0) 494 var val rt.Value 495 if err != nil { 496 val = rt.NilValue 497 } else if f.IsClosed() { 498 val = rt.StringValue("closed file") 499 } else { 500 val = rt.StringValue("file") 501 } 502 return c.PushingNext(t.Runtime, val), nil 503 } 504 505 func iowrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 506 return write(t.Runtime, rt.UserDataValue(getIoData(t.Runtime).defaultOutput), c) 507 } 508 509 func filewrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 510 if err := c.Check1Arg(); err != nil { 511 return nil, err 512 } 513 return write(t.Runtime, c.Arg(0), c) 514 } 515 516 func write(r *rt.Runtime, vf rt.Value, c *rt.GoCont) (rt.Cont, error) { 517 f, ok := ValueToFile(vf) 518 if !ok { 519 return nil, errors.New("#1 must be a file") 520 } 521 if f.IsClosed() { 522 return nil, errFileAlreadyClosed 523 } 524 var err error 525 for _, val := range c.Etc() { 526 switch val.Type() { 527 case rt.StringType: 528 case rt.IntType: 529 case rt.FloatType: 530 default: 531 return nil, errors.New("argument must be a string or a number") 532 } 533 s, _ := val.ToString() 534 if err = f.WriteString(s); err != nil { 535 break 536 } 537 } 538 next := c.Next() 539 if err != nil { 540 return r.ProcessIoError(next, err) 541 } else { 542 r.Push(next, vf) 543 } 544 return next, nil 545 } 546 547 func fileseek(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 548 if err := c.Check1Arg(); err != nil { 549 return nil, err 550 } 551 f, err := FileArg(c, 0) 552 if err != nil { 553 return nil, err 554 } 555 whence := io.SeekCurrent 556 offset := int64(0) 557 nargs := c.NArgs() 558 if nargs >= 2 { 559 whenceName, err := c.StringArg(1) 560 if err != nil { 561 return nil, err 562 } 563 switch whenceName { 564 case "cur": 565 whence = io.SeekCurrent 566 case "set": 567 whence = io.SeekStart 568 case "end": 569 whence = io.SeekEnd 570 default: 571 return nil, errors.New(`#1 must be "cur", "set" or "end"`) 572 } 573 } 574 if nargs >= 3 { 575 offsetI, err := c.IntArg(2) 576 if err != nil { 577 return nil, err 578 } 579 offset = int64(offsetI) 580 } 581 pos, ioErr := f.Seek(offset, whence) 582 next := c.Next() 583 if ioErr != nil { 584 t.Push1(next, rt.NilValue) 585 t.Push1(next, rt.StringValue(ioErr.Error())) 586 } else { 587 t.Push1(next, rt.IntValue(pos)) 588 } 589 return next, nil 590 } 591 592 func filesetvbuf(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 593 if err := c.CheckNArgs(2); err != nil { 594 return nil, err 595 } 596 f, err := FileArg(c, 0) 597 if err != nil { 598 return nil, err 599 } 600 mode, err := c.StringArg(1) 601 if err != nil { 602 return nil, err 603 } 604 var size int64 605 if c.NArgs() > 2 { 606 size, err = c.IntArg(2) 607 if err != nil { 608 return nil, err 609 } 610 } 611 bufErr := f.SetWriteBuffer(mode, int(size)) 612 if bufErr != nil { 613 return nil, bufErr 614 } 615 return c.PushingNext1(t.Runtime, rt.BoolValue(true)), nil 616 } 617 618 func tmpfile(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 619 f, err := TempFile(t.Runtime) 620 if err != nil { 621 return nil, err 622 } 623 fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable) 624 return c.PushingNext(t.Runtime, fv), nil 625 } 626 627 func tostring(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 628 if err := c.Check1Arg(); err != nil { 629 return nil, err 630 } 631 f, err := FileArg(c, 0) 632 if err != nil { 633 return nil, err 634 } 635 var s string 636 if f.IsClosed() { 637 s = "file (closed)" 638 } else { 639 s = fmt.Sprintf("file (%q)", f.Name()) 640 } 641 t.RequireBytes(len(s)) 642 return c.PushingNext(t.Runtime, rt.StringValue(s)), nil 643 }