Skip to content

Commit

Permalink
Add tty to exec command
Browse files Browse the repository at this point in the history
This required refactoring some logging so that logs are written by the exec
command and refactoring the display driver to do locking and notify the
listeners rather than the device itself.

Squashed commit of the following:

commit 26877e8
Author: Scott Moynes <sm@raspberrypi>
Date:   Thu Nov 30 16:35:44 2023 -0500

    Clean up display

commit eb0ac5c
Author: Scott Moynes <sm@raspberrypi>
Date:   Thu Nov 30 16:14:37 2023 -0500

    Add tty to exec command

commit 62edb2d
Author: Scott Moynes <sm@raspberrypi>
Date:   Thu Nov 30 14:36:49 2023 -0500

    Refactor listeners and ready flag to display device driver

commit 4408fc2
Author: Scott Moynes <sm@raspberrypi>
Date:   Thu Nov 30 13:28:35 2023 -0500

    Handle logger setup more cleanly
  • Loading branch information
Scott Moynes committed Nov 30, 2023
1 parent e1441c3 commit feac6a7
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 168 deletions.
2 changes: 2 additions & 0 deletions internal/cli/cmd/demo.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cmd

// demo.go holds a demonstration command.

import (
"context"
"errors"
Expand Down
104 changes: 66 additions & 38 deletions internal/cli/cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ import (
"github.com/smoynes/elsie/internal/encoding"
"github.com/smoynes/elsie/internal/log"
"github.com/smoynes/elsie/internal/monitor"
"github.com/smoynes/elsie/internal/tty"
"github.com/smoynes/elsie/internal/vm"
)

func Executor() cli.Command {
exec := &executor{
log: log.DefaultLogger(),
logLevel: log.Error,
logger: log.DefaultLogger(),
}

return exec
}

type executor struct {
log *log.Logger
logLevel log.Level
logger *log.Logger // Log destination
log string // Log output path
debug string // Debug log path
}

func (executor) Description() string {
Expand All @@ -46,17 +47,19 @@ Runs an executable in the emulator.`)
func (ex *executor) FlagSet() *cli.FlagSet {
fs := flag.NewFlagSet("exec", flag.ExitOnError)

fs.Func("loglevel", "set log `level`", func(s string) error {
return ex.logLevel.UnmarshalText([]byte(s))
})
fs.StringVar(&ex.log, "log", "", "write log to `file`")
fs.StringVar(&ex.debug, "debug", "", "write debug log `file`")

return fs
}

// Run executes the program.
func (ex *executor) Run(ctx context.Context, args []string, stdout io.Writer, logger *log.Logger,
) int {
log.LogLevel.Set(ex.logLevel)
if len(args) == 0 {
logger.Error("Missing object-code argument. Run elsie help exec for usage.")
return -1
}

// Code translated is encoded in a hex-based encoding.
code, err := ex.loadCode(args[0])
Expand All @@ -71,16 +74,49 @@ func (ex *executor) Run(ctx context.Context, args []string, stdout io.Writer, lo
ctx, cancelTimeout := context.WithTimeout(ctx, 10*time.Second)
defer cancelTimeout()

var (
logFile = os.Stderr
logLevel = log.Error
)

if ex.debug != "" {
logLevel = log.Debug

if logFile, err = os.OpenFile(ex.debug, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o600); err != nil {
err = fmt.Errorf("%s: %w", ex.debug, err)
}
} else if ex.log != "" {
logLevel = log.Info

if logFile, err = os.OpenFile(ex.log, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o600); err != nil {
err = fmt.Errorf("%s: %w", ex.log, err)
}
}

if err != nil {
ex.logger.Error(err.Error())
logger.Error(err.Error())

return -1
}

ex.logger = log.NewFormattedLogger(logFile)
log.SetDefault(ex.logger)
log.LogLevel.Set(logLevel)

ex.logger.Debug("Initializing machine")
logger.Debug("Initializing machine")

dispCh := make(chan rune, 1)
console, err := tty.NewConsole(os.Stdin, os.Stdout, os.Stderr)
if err != nil {
ex.logger.Error(err.Error())
return 1
}

machine := vm.New(
vm.WithLogger(logger),
vm.WithLogger(ex.logger),
monitor.WithDefaultSystemImage(),
vm.WithDisplayListener(func(displayed uint16) {
dispCh <- rune(displayed)
}),
console.WithTerminal(ctx),
)

loader := vm.NewLoader(machine)
Expand All @@ -91,37 +127,24 @@ func (ex *executor) Run(ctx context.Context, args []string, stdout io.Writer, lo
count += n

if err != nil {
logger.Error(err.Error())
ex.logger.Error(err.Error())
return 1
}
}

go func() {
logger.Debug("Starting display")

for {
select {
case disp := <-dispCh:
fmt.Printf("%c", disp)
case <-ctx.Done():
return
}
}
}()

logger.Debug("Loaded program", "file", args[0], "loaded", count)
ex.logger.Debug("Loaded program", "file", args[0], "loaded", count)

go func(cancel context.CancelCauseFunc) {
logger.Info("Starting machine")
ex.logger.Info("Starting machine")

err := machine.Run(ctx)

switch {
case errors.Is(err, context.DeadlineExceeded):
logger.Warn("Demo timeout")
ex.logger.Warn("Exec timeout")
return
case err != nil:
logger.Error(err.Error())
ex.logger.Error(err.Error())
cancel(err)

return
Expand All @@ -132,25 +155,30 @@ func (ex *executor) Run(ctx context.Context, args []string, stdout io.Writer, lo

<-ctx.Done()

close(dispCh)
console.Restore()

if err := ctx.Err(); errors.Is(err, context.DeadlineExceeded) {
logger.Error("Exec timeout!")
ex.logger.Error("Execution timeout")
logger.Error("Execution timeout")

return 2
} else if errors.Is(err, context.Canceled) {
logger.Info("Program completed")
ex.logger.Debug("Program completed")
logger.Debug("Program completed")
return 0
} else if err != nil {
ex.logger.Error("Program error", "ERR", err)
logger.Error("Program error", "ERR", err)
return 2
} else {
ex.logger.Info("Terminated")
logger.Info("Terminated")
return 0
}
}

func (ex executor) loadCode(fn string) ([]vm.ObjectCode, error) {
ex.log.Debug("Loading executable", "file", fn)
ex.logger.Debug("Loading executable", "file", fn)

file, err := os.Open(fn)
if err != nil {
Expand All @@ -159,16 +187,16 @@ func (ex executor) loadCode(fn string) ([]vm.ObjectCode, error) {

code, err := io.ReadAll(file)
if err != nil {
ex.log.Error(err.Error())
ex.logger.Error(err.Error())
return nil, err
}

ex.log.Debug("Loaded file", "bytes", len(code))
ex.logger.Debug("Loaded file", "bytes", len(code))

hex := encoding.HexEncoding{}

if err = hex.UnmarshalText(code); err != nil {
ex.log.Error(err.Error())
ex.logger.Error(err.Error())
return nil, err
}

Expand Down
11 changes: 7 additions & 4 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@ import (
)

var (
// Logger is the global, default logger.
logger = NewFormattedLogger(os.Stderr)

// DefaultLogger returns the default, global logger. During application startup components can
// call DefaultLogger and cache the result. The default will not change at runtime.
DefaultLogger = func() *Logger { return NewFormattedLogger(os.Stderr) }

// SetDefault overrides the default log output.
SetDefault = slog.SetDefault
DefaultLogger = func() *Logger { return logger }

// LogLevel is a variable holding the log level. It can be changed at runtime.
LogLevel = &slog.LevelVar{}
)

// SetDefault overrides the default log output.
func SetDefault(defaultLogger *Logger) { logger = defaultLogger }

// NewFormattedLogger returns a logger that uses a Handler to format and write logs to a Writer.
func NewFormattedLogger(out io.Writer) *Logger {
handler := NewHandler(out)
Expand Down
26 changes: 17 additions & 9 deletions internal/monitor/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ func WithSystemImage(image *SystemImage) vm.OptionFn {
// WithDefaultSystemImage initializes the machine with the default system image. You should probably
// use this.
func WithDefaultSystemImage() vm.OptionFn {
return WithSystemImage(NewSystemImage())
return func(machine *vm.LC3, late bool) {
if late {
logger := log.DefaultLogger()
image := NewSystemImage(logger)
loader := vm.NewLoader(machine)

if _, err := image.LoadTo(loader); err != nil {
panic(err) // TODO: return error
}
}
}
}

// SystemImage holds the initial state of memory for the machine. After construction, the image is
Expand All @@ -37,7 +47,7 @@ type SystemImage struct {
ISRs []Routine // Interrupt service routines are called from interrupt context.
Exceptions []Routine // Exception handlers are called in response to program faults.

log *log.Logger
logger *log.Logger
}

// Routine represents a system-defined system handler. Each routine's code is stored at an origin
Expand All @@ -52,9 +62,7 @@ type Routine struct {

// NewSystemImage creates a default system image including basic I/O system calls and exception
// handlers.
func NewSystemImage() *SystemImage {
logger := log.DefaultLogger()

func NewSystemImage(logger *log.Logger) *SystemImage {
data := vm.ObjectCode{
Orig: 0x0500,
Code: []vm.Word{},
Expand All @@ -72,18 +80,18 @@ func NewSystemImage() *SystemImage {
},
ISRs: []Routine{},
Exceptions: []Routine{},
log: logger,
logger: logger,
}
}

// LoadTo uses a loader to initialize the machine with the system image.
func (img *SystemImage) LoadTo(loader *vm.Loader) (uint16, error) {
img.log.Debug("Loading trap handlers")
img.logger.Debug("Loading trap handlers")

count := uint16(0)

for _, trap := range img.Traps {
img.log.Debug("Generating code",
img.logger.Debug("Generating code",
"trap", trap.Name,
"orig", trap.Orig,
"symbols", len(trap.Symbols),
Expand Down Expand Up @@ -127,7 +135,7 @@ func (img *SystemImage) LoadTo(loader *vm.Loader) (uint16, error) {
pc += 1
}

img.log.Debug("Loading vector",
img.logger.Debug("Loading vector",
"trap", trap.Name,
"orig", trap.Orig,
"symbols", len(trap.Symbols),
Expand Down
18 changes: 14 additions & 4 deletions internal/monitor/traps_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package monitor

import (
"context"
"errors"
"testing"
"time"
Expand Down Expand Up @@ -58,7 +59,7 @@ func TestTrap_Halt(tt *testing.T) {
Symbols: map[string]uint16{},
}

image := SystemImage{log: t.Logger(), Symbols: nil, Traps: []Routine{TrapHalt, putsRoutine}}
image := SystemImage{logger: t.Logger(), Symbols: nil, Traps: []Routine{TrapHalt, putsRoutine}}

machine := vm.New(
WithSystemImage(&image),
Expand Down Expand Up @@ -121,18 +122,23 @@ func TestTrap_Out(tt *testing.T) {

// We want to test the trap in isolation, without any other traps loaded.
image := SystemImage{
log: t.Logger(),
logger: t.Logger(),
Symbols: nil,
Traps: []Routine{
TrapOut,
},
}

displayed := make(chan uint16, 10)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)

machine := vm.New(
WithSystemImage(&image),
vm.WithDisplayListener(func(out uint16) {
select {
case <-ctx.Done():
return
case displayed <- out:
}
}),
Expand All @@ -144,6 +150,7 @@ func TestTrap_Out(tt *testing.T) {
Orig: 0x3000,
Code: []vm.Word{
vm.NewInstruction(vm.TRAP, uint16(vm.TrapOUT)).Encode(),
vm.NewInstruction(vm.TRAP, uint16(vm.TrapHALT)).Encode(),
},
}

Expand All @@ -161,13 +168,16 @@ func TestTrap_Out(tt *testing.T) {
if err != nil {
t.Errorf("Step error %s", err)
break
} else if machine.PC > 0x3000 {
} else if machine.PC > 0x3001 {
t.Log("Stepped to user code")
break
} else if !machine.MCR.Running() {
t.Log("Machine stopped")
break
}
}

cancel()
close(displayed)

vals := make([]uint16, 0, len(displayed))
Expand Down Expand Up @@ -202,7 +212,7 @@ func TestTrap_Puts(tt *testing.T) {

// We want to test this trap with another.
image := SystemImage{
log: t.Logger(),
logger: t.Logger(),
Symbols: nil,
Traps: []Routine{
TrapPuts,
Expand Down
Loading

0 comments on commit feac6a7

Please sign in to comment.