Add initial runtime prototype

This commit is contained in:
akp 2021-10-20 17:59:46 +01:00
parent 8cad290616
commit 61fede23ab
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
11 changed files with 581 additions and 0 deletions

14
go.mod Normal file
View file

@ -0,0 +1,14 @@
module github.com/codemicro/adventOfCode
go 1.17
require (
github.com/AlecAivazis/survey/v2 v2.3.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.3.3 // indirect
)

30
go.sum Normal file
View file

@ -0,0 +1,30 @@
github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -0,0 +1,62 @@
package challenge
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/codemicro/adventOfCode/runtime/util"
)
type Challenge struct {
Number int
Name string
Dir string
}
func (c *Challenge) String() string {
return fmt.Sprintf("%d - %s", c.Number, c.Name)
}
var challengeDirRegexp = regexp.MustCompile(`(?m)^(\d{2})-([a-zA-Z]+)$`)
func getChallengeDirs(dir string) ([]string, error) {
dirEntries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var res []string
for _, entry := range dirEntries {
if entry.IsDir() && challengeDirRegexp.MatchString(entry.Name()) {
res = append(res, entry.Name())
}
}
return res, nil
}
func ListingFromDir(sourceDir string) ([]*Challenge, error) {
dirs, err := getChallengeDirs(sourceDir)
if err != nil {
return nil, err
}
var o []*Challenge
for _, dir := range dirs {
x := strings.Split(dir, "-")
dayInt, _ := strconv.Atoi(x[0]) // error ignored because regex should have ensured this is ok
dayTitle := util.CamelToTitle(x[1])
o = append(o, &Challenge{
Number: dayInt,
Name: dayTitle,
Dir: filepath.Join(sourceDir, dir),
})
}
return o, nil
}

View file

@ -0,0 +1,36 @@
package challenge
import (
"encoding/json"
"io/ioutil"
)
type ChallengeInfo struct {
InputFile string `json:"inputFile"`
TestCases struct {
One []struct {
Input string `json:"input"`
Expected int64 `json:"expected"`
} `json:"one"`
Two []struct {
Input string `json:"input"`
Expected int `json:"expected"`
} `json:"two"`
} `json:"testCases"`
}
func LoadChallengeInfo(fname string) (*ChallengeInfo, error) {
fcont, err := ioutil.ReadFile(fname)
if err != nil {
return nil, err
}
c := new(ChallengeInfo)
err = json.Unmarshal(fcont, c)
if err != nil {
return nil, err
}
return c, nil
}

View file

@ -0,0 +1,26 @@
package challenge
import (
"github.com/codemicro/adventOfCode/runtime/runners"
"os"
"strings"
)
func (c *Challenge) GetImplementations() ([]string, error) {
dirEntries, err := os.ReadDir(c.Dir)
if err != nil {
return nil, err
}
var o []string
for _, de := range dirEntries {
if !de.IsDir() {
continue
}
if _, ok := runners.Available[strings.ToLower(de.Name())]; ok {
o = append(o, de.Name())
}
}
return o, nil
}

118
runtime/main.go Normal file
View file

@ -0,0 +1,118 @@
package main
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/codemicro/adventOfCode/runtime/challenge"
"github.com/codemicro/adventOfCode/runtime/runners"
"io/ioutil"
"path/filepath"
)
const (
challengeDir = "challenges"
challengeInfoFile = "info.json"
)
func userSelect(question string, choices []string) (int, error) {
var o string
prompt := &survey.Select{
Message: question,
Options: choices,
}
err := survey.AskOne(prompt, &o)
if err != nil {
return 0, err
}
for i, x := range choices {
if x == o {
return i, nil
}
}
return -1, nil
}
func run() error {
// List and select challenges
challenges, err := challenge.ListingFromDir(challengeDir)
if err != nil {
return err
}
var selectedChallengeIndex int
{
var opts []string
for _, c := range challenges {
opts = append(opts, c.String())
}
chosen, err := userSelect("Which challenge do you want to run?", opts)
if err != nil {
return err
}
selectedChallengeIndex = chosen
}
selectedChallenge := challenges[selectedChallengeIndex]
// List and select implementations
implementations, err := selectedChallenge.GetImplementations()
if err != nil {
return err
}
var selectedImplementationIndex int
{
var opts []string
for _, i := range implementations {
opts = append(opts, runners.RunnerNames[i])
}
chosen, err := userSelect("Which implementation do you want to use?", opts)
if err != nil {
return err
}
selectedImplementationIndex = chosen
}
selectedImplementation := implementations[selectedImplementationIndex]
// Load info.json file
challengeInfo, err := challenge.LoadChallengeInfo(filepath.Join(selectedChallenge.Dir, challengeInfoFile))
if err != nil {
return err
}
// Load challenge input
challengeInput, err := ioutil.ReadFile(filepath.Join(selectedChallenge.Dir, challengeInfo.InputFile))
if err != nil {
return err
}
challengeInputString := string(challengeInput)
runner := runners.Available[selectedImplementation](selectedChallenge.Dir)
runner.Queue(&runners.Task{
Part: 1,
Input: challengeInputString,
})
for roe := range runner.Run() {
if roe.Error != nil {
return roe.Error
}
fmt.Println(*roe.Result)
}
return nil
}
func main() {
if err := run(); err != nil {
panic(err)
}
}

131
runtime/runners/comm.go Normal file
View file

@ -0,0 +1,131 @@
package runners
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os/exec"
"strings"
"sync"
)
type Task struct {
Part Part `json:"part"`
Input string `json:"input"`
OutputDir string `json:"output_dir,omitempty"`
}
type Result struct {
TaskNumber int `json:"task_n"`
Ok bool `json:"ok"`
Output string `json:"output"`
Duration float32 `json:"duration"`
}
func makeErrorChan(err error) chan ResultOrError {
c := make(chan ResultOrError, 1)
c <- ResultOrError{Error: err}
close(c)
return c
}
// custom writer type
type customWriter struct {
pending []byte
entries [][]byte
mux sync.Mutex
}
func (c *customWriter) Write(b []byte) (int, error) {
var n int
c.mux.Lock()
for _, x := range b {
if x == '\n' {
c.entries = append(c.entries, c.pending)
c.pending = nil
} else {
c.pending = append(c.pending, x)
}
n += 1
}
c.mux.Unlock()
return n, nil
}
func (c *customWriter) GetEntry() ([]byte, error) {
c.mux.Lock()
defer c.mux.Unlock()
if len(c.entries) == 0 {
return nil, errors.New("no entries")
}
var x []byte
x, c.entries = c.entries[0], c.entries[1:]
return x, nil
}
func readResultsFromCommand(cmd *exec.Cmd, cleanupFn func()) chan ResultOrError {
stdoutWriter := &customWriter{}
stderrBuffer := new(bytes.Buffer)
cmd.Stdout = stdoutWriter
cmd.Stderr = stderrBuffer
err := cmd.Start()
if err != nil {
return makeErrorChan(err)
}
// Command status listener
status := make(chan bool) // true if success, false if failure
go func() {
_ = cmd.Wait()
status <- cmd.ProcessState.Success()
close(status)
}()
// Now let's read some results
c := make(chan ResultOrError)
go func() {
readerLoop:
for {
inp, err := stdoutWriter.GetEntry()
// will return an error if there is nothing to retrieve
if err == nil {
res := new(Result)
err = json.Unmarshal(inp, res)
if err != nil {
// echo anything that won't parse to stdout (this lets us add debug print statements)
fmt.Printf("AA %#v\n", strings.TrimSpace(string(inp)))
} else {
c <- ResultOrError{Result: res}
}
}
select {
case successfulFinish := <-status:
if !successfulFinish {
c <- ResultOrError{Error: errors.New("run failed: " + stderrBuffer.String())}
}
break readerLoop
default:
}
}
close(c)
if cleanupFn != nil {
cleanupFn()
}
}()
return c
}

View file

@ -0,0 +1,46 @@
from py import Challenge
import time
import json
TASKS_STR = """{{ .TasksJSON }}"""
TASKS = json.loads(TASKS_STR)
def send_result(task_number, ok, output, duration):
print(json.dumps({
"task_n": task_number,
"ok": ok,
"output": str(output) if output is not None else "",
"duration": float(duration),
}), flush=True)
for task_number, task in enumerate(TASKS):
taskPart = task["part"]
run = None
if taskPart == 1:
run = lambda: Challenge().one(task["input"])
elif taskPart == 2:
run = lambda: Challenge().two(task["input"])
elif taskPart == 3:
run = lambda: Challenge().vis(task["input"], task["output_dir"])
else:
send_result(task_number, False, "unknown task part", 0)
continue
start_time = time.time()
res = None
err = None
try:
res = run()
except Exception as e:
err = f"{type(e)}: {e}"
end_time = time.time()
running_time = end_time-start_time
if err is not None:
send_result(task_number, False, err, running_time)
else:
send_result(task_number, True, res, running_time)

View file

@ -0,0 +1,70 @@
package runners
import (
"bytes"
_ "embed"
"encoding/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"text/template"
)
const python3Installation = "python3"
type pythonRunner struct {
dir string
tasks []*Task
}
func newPythonRunner(dir string) Runner {
return &pythonRunner{
dir: dir,
}
}
func (p *pythonRunner) Queue(task *Task) {
p.tasks = append(p.tasks, task)
}
//go:embed interface/python.py
var pythonInterface string
func (p *pythonRunner) Run() chan ResultOrError {
wrapperFilename := "runtime-wrapper.py"
wrapperFilepath := filepath.Join(p.dir, wrapperFilename)
// Generate interaction code
taskJSON, err := json.Marshal(p.tasks)
if err != nil {
return makeErrorChan(err)
}
interactionCodeBuffer := new(bytes.Buffer)
{
templ := template.Must(template.New("").Parse(pythonInterface))
err := templ.Execute(interactionCodeBuffer, struct{
TasksJSON string
}{string(taskJSON)})
if err != nil {
return makeErrorChan(err)
}
}
// Save interaction code
err = ioutil.WriteFile(wrapperFilepath, interactionCodeBuffer.Bytes(), 0644)
if err != nil {
return makeErrorChan(err)
}
// Run Python and gather output
cmd := exec.Command(python3Installation, "-B", wrapperFilename) // -B prevents .pyc files from being written
cmd.Dir = p.dir
return readResultsFromCommand(cmd, func() {
// Remove leftover files
_ = os.Remove(wrapperFilepath)
})
}

View file

@ -0,0 +1,29 @@
package runners
type Part uint8
const (
PartOne Part = iota + 1
PartTwo
Visualise
)
type Runner interface {
Queue(task *Task)
Run() chan ResultOrError
}
type ResultOrError struct {
Result *Result
Error error
}
type RunnerCreator func(dir string) Runner
var Available = map[string]RunnerCreator{
"py": newPythonRunner,
}
var RunnerNames = map[string]string{
"py": "Python",
}

19
runtime/util/util.go Normal file
View file

@ -0,0 +1,19 @@
package util
import (
"unicode"
)
func CamelToTitle(x string) string {
var o string
for i, char := range x {
if i == 0 {
o += string(unicode.ToUpper(char))
} else if unicode.IsUpper(char) {
o += " " + string(char)
} else {
o += string(char)
}
}
return o
}