/
session.go
161 lines (137 loc) · 3.42 KB
/
session.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package inform
import (
// Standard library
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"time"
// Third-party packages
"github.com/pkg/errors"
)
// Default arguments for executable dependencies.
var (
// The prefix used for Frotz meta-commands.
frotzMetaPrefix = "\\"
frotzArgs = []string{"-r", "lt", "-r", "cm", "-r", "ch1", "-p", "-m", "-R"}
)
type Session struct {
path string
name string
proc *os.Process
in io.WriteCloser
out io.ReadCloser
err io.ReadCloser
}
func (s *Session) Run(cmd string) error {
switch strings.ToLower(cmd) {
case "restore":
return errors.New("TODO: Implement restoring")
case "save":
return errors.New("TODO: Implement saving")
case "\\x", "quit":
return errors.New("TODO: Implement session stopping")
case "script", "unscript":
return errors.New("transcripts are disabled")
case "\\<", "\\>", "\\^", "\\.": // Cursor motion
case "\\1", "\\2", "\\3", "\\4", "\\5", "\\6", "\\7", "\\8", "\\9", "\\0": // F1 - F10
case "\\n", "\\u": // Frotz hot-keys
default:
if strings.HasPrefix(cmd, frotzMetaPrefix) {
return errors.New("meta-commands are disabled")
}
}
if _, err := s.in.Write(append([]byte(cmd), '\n')); err != nil {
return errors.New("failed writing command")
}
return s.Error()
}
func (s *Session) Output() string {
var buf = bytes.TrimSuffix(readPipe(s.out), []byte{'\n', '>'})
return string(buf)
}
func (s *Session) Error() error {
var buf = bytes.ReplaceAll(readPipe(s.err), []byte{'\n'}, []byte{':', ' '})
if len(buf) > 0 {
return errors.New(string(buf))
}
return nil
}
func (s *Session) Start(ctx context.Context, conf *Config) error {
var err error
var cmd = exec.CommandContext(ctx, conf.DumbFrotz, append(frotzArgs, s.path, s.name)...)
if s.in, err = cmd.StdinPipe(); err != nil {
return errors.Wrap(err, "starting session failed")
} else if s.out, err = cmd.StdoutPipe(); err != nil {
return errors.Wrap(err, "starting session failed")
} else if s.err, err = cmd.StderrPipe(); err != nil {
return errors.Wrap(err, "starting session failed")
} else if err = cmd.Start(); err != nil {
return errors.Wrap(err, "starting session failed")
} else if err := s.Error(); err != nil {
return err
}
s.proc = cmd.Process
return nil
}
func (s *Session) Close() error {
var err error
if s.proc != nil {
err = s.proc.Kill()
s.proc = nil
}
return err
}
func NewSession(story *Story) (*Session, error) {
dir, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("%s-%s-%s-*", keyPrefix, story.AuthorID, story.Name))
if err != nil {
return nil, errors.Wrap(err, "creating temporary directory failed")
}
f, err := os.Create(path.Join(dir, "output.z8"))
if err != nil {
return nil, errors.Wrap(err, "writing temporary story file failed")
}
defer f.Close()
if _, err = f.Write(story.Build); err != nil {
return nil, errors.Wrap(err, "writing temporary story file failed")
}
return &Session{
path: dir,
name: f.Name(),
}, nil
}
func readPipe(r io.Reader) []byte {
var chunks = make(chan []byte)
go func() {
var chunk = make([]byte, 1024)
for {
n, err := r.Read(chunk)
if err != nil || n == 0 {
close(chunks)
return
}
chunks <- chunk[:n]
if n < 1024 {
close(chunks)
return
}
}
}()
var buf []byte
for {
select {
case chunk, ok := <-chunks:
if !ok {
return buf
}
buf = append(buf, chunk...)
case <-time.After(10 * time.Millisecond):
return buf
}
}
}