Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Milestone 1 - Core Functionality #1

Merged
merged 26 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ec989f
add line to gitignore
Mar 18, 2021
ec3b391
Add unimplemented core
Mar 18, 2021
04e3134
add proc package for handling processes and their output.
Mar 18, 2021
bc082b5
make Core start, stop, and store processes
Mar 18, 2021
bd5bcd8
move bitbox.ProcStatus to proc.ProcStatus
Mar 18, 2021
792fc71
implement proc.Status(),
Mar 18, 2021
9a2465c
implement core.Status(id)
Mar 18, 2021
3cc6a18
call exec.Cmd.Wait to ensure that cmd.ProcessState is going to be pop…
Mar 18, 2021
4eebc6b
Change order of ProcStatus enum to communicate that Exited is the "mo…
Mar 18, 2021
a8522b7
Fix ridiculous typo. Lock() should be Unlock()
Mar 18, 2021
11d7c4c
move ProcOutput to proc package
Mar 18, 2021
dd689d7
Fix proc.ProcStatus.String()
Mar 18, 2021
5bbc5bd
Add ProcOutput constructors
Mar 19, 2021
4f6c0e7
Implement proc.Proc.Query() and BitBox.Query()
Mar 19, 2021
b4e5d12
Add missing returns for errors case.
Mar 19, 2021
7aeaa6d
Change proc to hold file names for output instead of file handles
Mar 19, 2021
79511f6
change bitbox.Core lock to embedded sync.RWMutex
Mar 23, 2021
64a452e
remove call to exec.FindPath because exec.Command uses it under the hood
Mar 23, 2021
58e140c
Fix unclosed files for polling of processes
Mar 23, 2021
65fd5b5
pollRead reads files in 1024 byte portions
Mar 23, 2021
11f4a51
Combine Stdout and Stderr into single output file
Mar 23, 2021
1b24110
split finishRead() out from pollRead()
Mar 23, 2021
2f80c8e
proc.Status() now more closely follows ProcessState.ExitCode()
Mar 23, 2021
e456811
add proc.waitMutex to protect cmd.Wait()
Mar 23, 2021
4e32788
make Core hold *proc.Proc instead of proc.Proc
Mar 23, 2021
9ab1e09
Remove unnecessary go routine.
Mar 23, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# VS Code history
# VS Code tools
*.code-workspace
.history*


83 changes: 83 additions & 0 deletions core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package bitbox

import (
"fmt"
"sync"

"github.com/google/uuid"
"github.com/jmbarzee/bitbox/proc"
)

// Core offers the central functionality of BitBox.
// Core supports basic process control and interaction.
type Core struct {
sync.RWMutex
processes map[uuid.UUID]*proc.Proc
}

// Start initiates a process.
func (c *Core) Start(cmd string, args ...string) (uuid.UUID, error) {

id := uuid.New()
proc, err := proc.NewProc(cmd, args...)
if err != nil {
return uuid.UUID{}, c.newError("Start", err)
}

c.Lock()
c.processes[id] = proc // Chance of colision (16 byte id, so roughly 2^128 chance)
c.Unlock()
return id, nil
}

// Stop halts a process.
func (c *Core) Stop(id uuid.UUID) error {
var p *proc.Proc
var err error

if p, err = c.findProcess(id); err != nil {
return c.newError("Stop", err)
}
if err = p.Stop(); err != nil {
return c.newError("Stop", err)
}
return nil
}

// Status returns the status of the process.
func (c *Core) Status(id uuid.UUID) (proc.ProcStatus, error) {
var p *proc.Proc
var err error

if p, err = c.findProcess(id); err != nil {
return proc.Exited, c.newError("Status", err)
}

return p.Status(), nil
}

// Query streams the output/result of a process.
func (c *Core) Query(id uuid.UUID) (<-chan proc.ProcOutput, error) {
var p *proc.Proc
var err error

if p, err = c.findProcess(id); err != nil {
return nil, c.newError("Query", err)
}

return p.Query()
}

func (c *Core) findProcess(id uuid.UUID) (*proc.Proc, error) {
c.RLock()
defer c.RUnlock()
p, ok := c.processes[id]
if !ok {
return nil, fmt.Errorf("could not find specified process %v", id)
}
return p, nil
}

func (*Core) newError(action string, err error) error {
return fmt.Errorf("could not %v process: %w", action, err)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/jmbarzee/bitbox

go 1.15

require github.com/google/uuid v1.2.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
227 changes: 227 additions & 0 deletions proc/proc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// proc offers control over arbitrary processes.
// proc depends heavily on os/exec.
// If/when proc needs to do resource control & isolation
// its probable that os/exec will need to be replaced with
// either the os package itself or syscall.
package proc

// Fingers crossed that its easier than rewriting os.ForkExec

import (
"context"
"io/ioutil"
"os"
"os/exec"
"sync"
"time"
)

const outputReadRefreshInterval = time.Millisecond * 10

// Proc is a BitBox process.
// Stderr and Stdout are dumped to temp files on disk.
type Proc struct {
outputFileName string
waitMutex sync.Mutex // See https://github.com/golang/go/issues/28461
cmd *exec.Cmd
}

// NewProc constructs and begins process.
// Stdout & Stderr of the new process are pointed at temp files.
// The tempfileNames are acessable through the coresponding members.
func NewProc(cmdName string, args ...string) (*Proc, error) {

cmd := exec.Command(cmdName, args...)

var err error
var output *os.File
if output, err = ioutil.TempFile("", ""); err != nil {
return nil, err
}
cmd.Stdout = output
cmd.Stderr = output
outputFileName := output.Name()

if err = cmd.Start(); err != nil {
return nil, err
}

p := &Proc{
outputFileName: outputFileName,
cmd: cmd,
}

go func() {
// Wait on the cmd to make sure resources get released
p.waitMutex.Lock()
cmd.Wait()
p.waitMutex.Unlock()
}()

return p, nil
}

// Stop causes the running process to exit (sigkill) and closes the related resources.
func (p *Proc) Stop() error {
if err := p.cmd.Process.Kill(); err != nil {
jmbarzee marked this conversation as resolved.
Show resolved Hide resolved
// processes which have ended return errors
return err
}
// Throw away the error from Wait() because an error is returned if either:
// 1. Wait() was already called (cool, we just use it to know that the process exited)
// 2. The process returned a non-zero exit code (cool, we will return any exit code)
p.waitMutex.Lock()
p.cmd.Wait()
jmbarzee marked this conversation as resolved.
Show resolved Hide resolved
p.waitMutex.Unlock()
return nil
}

// Status returns the status of the process.
func (p *Proc) Status() ProcStatus {
if p.cmd.ProcessState == nil {
return Running
}

if p.cmd.ProcessState.ExitCode() < 0 {
return Stopped
}
return Exited
}

// ProcStatus is the status of a process.
type ProcStatus int

const (
// Running indicates that the process is running.
Running ProcStatus = iota
// Exited indicates that the process returned an exit code.
Exited
// Stopped indicates that the process was terminated by a signal.
Stopped
)

func (ps ProcStatus) String() string {
return [...]string{"Running", "Exited", "Stopped"}[ps]
}

// Query streams output from the process to the returned channel.
// The Stdout and Stderr files are opened for reads and polled until
// a third routine finds that the process has exited.
// The third routine cancels the context of the pollReads.
// After the read routines finish the third routine sends the ExitCode and closes the channel.
func (p *Proc) Query() (<-chan ProcOutput, error) {
flags := os.O_RDONLY | os.O_SYNC
outputFile, err := os.OpenFile(p.outputFileName, flags, 0600)
if err != nil {
return nil, err
}

ctx, cancel := context.WithCancel(context.Background())
stream := make(chan ProcOutput)
wg := &sync.WaitGroup{}
wg.Add(1)

go func() {
pollRead(ctx, outputFile, stream)
finishRead(outputFile, stream)
outputFile.Close()
wg.Done()
}()

go func() {
// Throw away the error from Wait() because an error is returned if either:
// 1. Wait() was already called (cool, we just use it to know that the process completed)
// 2. The process returned a non-zero exit code (cool, we will return any exit code)
p.waitMutex.Lock()
p.cmd.Wait()
p.waitMutex.Unlock()

cancel()
jmbarzee marked this conversation as resolved.
Show resolved Hide resolved
wg.Wait()
stream <- &ProcOutput_ExitCode{
ExitCode: uint32(p.cmd.ProcessState.ExitCode()),
}
close(stream)
}()

return stream, nil
}

func pollRead(
ctx context.Context,
file *os.File,
stream chan<- ProcOutput,
) {
buf := make([]byte, 1024)
ticker := time.NewTicker(outputReadRefreshInterval)

for {
select {
case <-ticker.C:
// ReadLoop
for {
n, err := file.Read(buf)
if err != nil {
// TODO: should we log the error somehow?
return
}
if n == 0 {
break // move to wait for ticker or context to end
}
stream <- newProcOutput_Stdouterr(buf)
}
case <-ctx.Done():
return
}
}
}

func finishRead(
file *os.File,
stream chan<- ProcOutput,
) {
buf := make([]byte, 1024)

for {
n, err := file.Read(buf)
if err != nil {
// TODO: should we log the error somehow?
break
}
if n == 0 {
break
}
stream <- newProcOutput_Stdouterr(buf)
}
}

// ProcOutput is any output from a process.
type ProcOutput interface {
isProcOutput()
}

var _ ProcOutput = (*ProcOutput_Stdouterr)(nil)

// ProcOutput_Stdouterr is any output from the process which was written to Stdout and Stderr.
type ProcOutput_Stdouterr struct {
// Stdout is a series of characters sent to Stdout by a process.
Output string
}

func newProcOutput_Stdouterr(b []byte) ProcOutput {
return &ProcOutput_Stdouterr{
Output: (string)(b),
}
}

func (*ProcOutput_Stdouterr) isProcOutput() {}

var _ ProcOutput = (*ProcOutput_ExitCode)(nil)

// ProcOutput_ExitCode is any output from the process which was written to Stderr.
type ProcOutput_ExitCode struct {
// ExitCode is the exit code of a process.
ExitCode uint32
}

func (*ProcOutput_ExitCode) isProcOutput() {}
5 changes: 0 additions & 5 deletions server.go

This file was deleted.