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:
parent
996ecb9f80
commit
3455f491ca
104 changed files with 836 additions and 511 deletions
56
pkg/cancel/context.go
Normal file
56
pkg/cancel/context.go
Normal 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
57
pkg/cancel/copy.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue