Add package stub.

This commit is contained in:
Qi Xiao 2016-02-21 15:31:40 +01:00
parent 172fa49056
commit 4c0919b130
3 changed files with 163 additions and 0 deletions

View File

@ -151,5 +151,6 @@ The adjective for elvish is also "elvish", not "elvishy" and definitely not "elv
|parse|[![parse](https://gocover.io/_badge/github.com/elves/elvish/parse/)](https://gocover.io/github.com/elves/elvish/parse/)|
|run|[![run](https://gocover.io/_badge/github.com/elves/elvish/run/)](https://gocover.io/github.com/elves/elvish/run/)|
|store|[![store](https://gocover.io/_badge/github.com/elves/elvish/store/)](https://gocover.io/github.com/elves/elvish/store/)|
|stub|[![stub](https://gocover.io/_badge/github.com/elves/elvish/stub/)](https://gocover.io/github.com/elves/elvish/stub/)|
|sys|[![sys](https://gocover.io/_badge/github.com/elves/elvish/sys/)](https://gocover.io/github.com/elves/elvish/sys/)|
|util|[![util](https://gocover.io/_badge/github.com/elves/elvish/util/)](https://gocover.io/github.com/elves/elvish/util/)|

127
stub/stub.go Normal file
View File

@ -0,0 +1,127 @@
// Package stub is used to start and manage an elvish-stub process.
package stub
import (
"fmt"
"io"
"os"
"path"
"strings"
"syscall"
"github.com/elves/elvish/util"
)
var stubname = "elvish-stub"
type Stub struct {
process *os.Process
// write is the other end of stdin of the stub.
write *os.File
// read is the other end of stdout of the stub.
read *os.File
sigch chan os.Signal
statech chan struct{}
}
var stubEnv = []string{"A=BCDEFGHIJKLMNOPQRSTUVWXYZ"}
// NewStub spawns a new stub. The specified stderr is used for the subprocess.
func NewStub(stderr *os.File) (*Stub, error) {
// Find stub.
stubpath, err := searchStub()
if err != nil {
return nil, fmt.Errorf("search: %v", err)
}
// Make pipes.
stdin, write, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("pipe: %v", err)
}
read, stdout, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("pipe: %v", err)
}
// Spawn stub.
process, err := os.StartProcess(stubpath, []string{stubpath},
&os.ProcAttr{Env: stubEnv, Files: []*os.File{stdin, stdout, stderr}})
if err != nil {
return nil, fmt.Errorf("spawn: %v", err)
}
// Wait for startup message.
_, err = fmt.Fscanf(read, "ok\n")
if err != nil {
return nil, fmt.Errorf("read startup msg: %v", err)
}
// Spawn signal relayer and waiter.
sigch := make(chan os.Signal)
statech := make(chan struct{})
go relaySignals(read, sigch)
go wait(process, statech)
return &Stub{process, write, read, sigch, statech}, nil
}
func searchStub() (string, error) {
// os.Args[0] contains an absolute path. Find elvish-stub in the same
// directory where elvish was started.
if len(os.Args) > 0 && path.IsAbs(os.Args[0]) {
stubpath := path.Join(path.Dir(os.Args[0]), stubname)
if util.IsExecutable(stubpath) {
return stubpath, nil
}
}
return util.Search(strings.Split(os.Getenv("PATH"), ":"), stubname)
}
func (stub *Stub) Process() *os.Process {
return stub.process
}
// Terminate terminates the stub.
func (stub *Stub) Terminate() {
stub.write.Close()
}
// SetTitle sets the title of the stub.
func (stub *Stub) SetTitle(s string) {
stub.write.WriteString(s + "\n")
}
// Signals returns a channel into which signals sent to the stub are relayed.
func (stub *Stub) Signals() <-chan os.Signal {
return stub.sigch
}
// State returns a channel that is closed when the stub exits.
func (stub *Stub) State() <-chan struct{} {
return stub.statech
}
// relaySignals relays output of the stub to sigch, assuming that outputs
// represent signal numbers.
func relaySignals(reader io.Reader, sigch chan<- os.Signal) {
for {
var signum int
_, err := fmt.Fscanf(reader, "%d", &signum)
if err != nil {
// XXX Swallow error.
return
}
sigch <- syscall.Signal(signum)
}
}
func wait(proc *os.Process, ch chan<- struct{}) {
for {
state, err := proc.Wait()
if err != nil || state.Exited() {
break
}
}
close(ch)
}

35
stub/stub_test.go Normal file
View File

@ -0,0 +1,35 @@
package stub
import (
"os"
"syscall"
"testing"
"time"
)
func TestStub(t *testing.T) {
stub, err := NewStub(os.Stderr)
if err != nil {
t.Skip(err)
}
proc := stub.Process()
// Signals should be relayed.
proc.Signal(syscall.SIGINT)
select {
case sig := <-stub.Signals():
if sig != syscall.SIGINT {
t.Errorf("got %v, want SIGINT", sig)
}
case <-time.After(time.Millisecond * 10):
t.Errorf("signal not relayed after 10ms")
}
// Calling Terminate should really terminate the process.
stub.Terminate()
select {
case <-stub.State():
case <-time.After(time.Millisecond * 10):
t.Errorf("stub didn't exit within 10ms")
}
}