Add context

This commit adds and passes context around to (almost) every function.
This allows cancelling (almost) every function call globally.
This commit is contained in:
Tom Wiesing 2022-11-28 13:30:08 +01:00
parent 996ecb9f80
commit 3455f491ca
No known key found for this signature in database
104 changed files with 836 additions and 511 deletions

56
pkg/cancel/context.go Normal file
View file

@ -0,0 +1,56 @@
package cancel
import (
"context"
)
// WithContext executes f and returns the returns the return value and nil.
//
// If the context is closed before f returns, invokes cancel and returns f(), ctx.Err().
//
// In general, WithContext always waits for f() to return even if cancel was called.
// As a special case if a closed context is passed, f is not invoked.
//
// allowcancel must be called by f exactly once, as soon as the cancel function may be invoked.
func WithContext[T any](ctx context.Context, f func(allowcancel func()) T, cancel func()) (t T, err error) {
t, _, err = WithContext2(ctx, func(start func()) (T, struct{}) {
return f(start), struct{}{}
}, cancel)
return
}
// WithContext2 is exactly like WithContext, but takes a function returning two parameters.
func WithContext2[T1, T2 any](ctx context.Context, f func(start func()) (T1, T2), cancel func()) (t1 T1, t2 T2, err error) {
// context is already closed, don't even try invoking it.
if err := ctx.Err(); err != nil {
return t1, t2, err
}
cancelable := make(chan struct{}, 1)
done := make(chan struct{})
go func() {
defer close(done)
defer close(cancelable)
t1, t2 = f(func() {
cancelable <- struct{}{}
})
}()
select {
case <-done:
// the function has exited regularly
// nothing to be done
case <-ctx.Done():
// context was cancelled
<-cancelable
cancel()
// still wait for it to be done!
<-done
err = ctx.Err()
}
return
}

57
pkg/cancel/copy.go Normal file
View file

@ -0,0 +1,57 @@
package cancel
import (
"context"
"io"
"time"
)
type SetDeadline interface {
SetDeadline(t time.Time)
}
type SetReadDeadline interface {
SetReadDeadline(t time.Time) error
}
type SetWriteDeadline interface {
SetWriteDeadline(t time.Time) error
}
// Copy reads from src, and copies to dst.
//
// If the context is closed before src is closed, attempts to close the underlying reader and writer.
func Copy(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) {
// if the context has a deadline, the propanate that deadline to the underyling file.
// this might cause the read call to not block.
if deadline, ok := ctx.Deadline(); ok {
var zero time.Time
if file, ok := src.(SetReadDeadline); ok {
file.SetReadDeadline(deadline)
defer file.SetReadDeadline(zero)
} else if file, ok := src.(SetDeadline); ok {
file.SetDeadline(deadline)
defer file.SetDeadline(zero)
}
if file, ok := dst.(SetWriteDeadline); ok {
file.SetWriteDeadline(deadline)
defer file.SetWriteDeadline(zero)
} else if file, ok := dst.(SetDeadline); ok {
file.SetDeadline(deadline)
defer file.SetDeadline(zero)
}
}
written, err, _ = WithContext2(ctx, func(start func()) (int64, error) {
start()
return io.Copy(dst, src)
}, func() {
if closer, ok := src.(io.Closer); ok {
closer.Close()
}
})
return written, err
}

View file

@ -45,7 +45,7 @@ type Environment interface {
DialContext(context context.Context, network, address string) (net.Conn, error)
Executable() (string, error)
Exec(io stream.IOStream, workdir string, exe string, argv ...string) int
Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) int
LookPathAbs(name string) (string, error)
}

View file

@ -2,6 +2,7 @@ package environment
import (
"bytes"
"context"
"io"
"io/fs"
"os"
@ -64,6 +65,6 @@ func ReadFile(env Environment, path string) ([]byte, error) {
}
// MustExec is like Exec, except that it returns true if the command exited successfully, and else false.
func MustExec(env Environment, io stream.IOStream, workdir string, exe string, argv ...string) bool {
return env.Exec(io, workdir, exe, argv...) == 0
func MustExec(ctx context.Context, env Environment, io stream.IOStream, workdir string, exe string, argv ...string) bool {
return env.Exec(ctx, io, workdir, exe, argv...) == 0
}

View file

@ -1,8 +1,10 @@
package environment
import (
"context"
"os/exec"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/tkw1536/goprogram/stream"
)
@ -10,7 +12,7 @@ import (
//
// If the command executes, it's exit code will be returned.
// If the command can not be executed, returns [ExecCommandError].
func (*Native) Exec(io stream.IOStream, workdir string, exe string, argv ...string) int {
func (*Native) Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) int {
// setup the command
cmd := exec.Command(exe, argv...)
cmd.Dir = workdir
@ -18,8 +20,27 @@ func (*Native) Exec(io stream.IOStream, workdir string, exe string, argv ...stri
cmd.Stdout = io.Stdout
cmd.Stderr = io.Stderr
// run it
err := cmd.Run()
// run the process in a cancelable fashion
err, cErr := cancel.WithContext(ctx, func(cancelable func()) error {
// start the process
err := cmd.Start()
if err != nil {
return err
}
// allow it to be cancellable
cancelable()
// and wait for the rest of the process
return cmd.Wait()
}, func() {
if cmd.Process != nil {
cmd.Process.Kill()
}
})
if err == nil {
err = cErr
}
// non-zero exit
if err, ok := err.(*exec.ExitError); ok {

View file

@ -1,11 +1,12 @@
package fsx
import (
"context"
"errors"
"io"
"io/fs"
"path/filepath"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
)
@ -15,7 +16,12 @@ var ErrCopySameFile = errors.New("src and dst must be different")
// When src points to a symbolic link, will copy the symbolic link.
//
// When dst and src are the same file, returns ErrCopySameFile.
func CopyFile(env environment.Environment, dst, src string) error {
// When ctx is closed, the file is not copied.
func CopyFile(ctx context.Context, env environment.Environment, dst, src string) error {
if err := ctx.Err(); err != nil {
return err
}
if SameFile(env, src, dst) {
return ErrCopySameFile
}
@ -41,13 +47,17 @@ func CopyFile(env environment.Environment, dst, src string) error {
defer dstFile.Close()
// and do the copy!
_, err = io.Copy(dstFile, srcFile)
_, err = cancel.Copy(ctx, dstFile, srcFile)
return err
}
// CopyLink copies a link from src to dst.
// If dst already exists, it is deleted and then re-created.
func CopyLink(env environment.Environment, dst, src string) error {
func CopyLink(ctx context.Context, env environment.Environment, dst, src string) error {
if err := ctx.Err(); err != nil {
return err
}
// if they're the same file that is an error
if SameFile(env, dst, src) {
return ErrCopySameFile
@ -73,12 +83,13 @@ func CopyLink(env environment.Environment, dst, src string) error {
var ErrDstFile = errors.New("dst is a file")
// CopyDirectory copies the directory src to dst recursively.
// Copying is aborted when ctx is closed.
//
// Existing files and directories are overwritten.
// When a directory already exists, additional files are not deleted.
//
// onCopy, when not nil, is called for each file or directory being copied.
func CopyDirectory(env environment.Environment, dst, src string, onCopy func(dst, src string)) error {
func CopyDirectory(ctx context.Context, env environment.Environment, dst, src string, onCopy func(dst, src string)) error {
// sanity checks
if SameFile(env, src, dst) {
return ErrCopySameFile
@ -88,10 +99,16 @@ func CopyDirectory(env environment.Environment, dst, src string, onCopy func(dst
}
return env.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
// someone previously returned an error
if err != nil {
return err
}
// context was closed
if err := ctx.Err(); err != nil {
return err
}
// determine the real target path
var relpath string
relpath, err = filepath.Rel(src, path)
@ -113,12 +130,12 @@ func CopyDirectory(env environment.Environment, dst, src string, onCopy func(dst
// if we have a symbolic link, copy the link!
if info.Mode()&fs.ModeSymlink != 0 {
return CopyLink(env, dst, path)
return CopyLink(ctx, env, dst, path)
}
// if we got a file, we should copy it normally
if !d.IsDir() {
return CopyFile(env, dst, path)
return CopyFile(ctx, env, dst, path)
}
// create the directory, but ignore an error if the directory already exists.