diff --git a/README.md b/README.md index b153f70..3894f18 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ Run `tb --help` to see the commands available. Run `tb --help` to get help `tb` can be configured through the `.tbrc.yml` file located in your home directory. `tb` will automatically create a basic `.tbrc.yml` for you if one doesn't exist. +### Timeout + +You can specify a timeout value in `.tbrc.yml`. This value will be used to kill any operation that exceeds the given time. All you need to do is set `timeoutSeconds: 1000` in your `.tbrc.yml`. Allowed values are 5 to 3600 inclusive. If `timeoutSeconds` is not specified or set to 0, `tb` will default to 3600 seconds (i.e 60 minutes). + ### Toggling experimental mode To enable experimental mode set the `experimental` field to `true`. Experimental mode will give you access to any new features that are still in the process of being tested. Please be aware that you may encounter bugs with these features as they have not yet been deemed ready for general use. diff --git a/config/config.go b/config/config.go index e2aac1b..20a4d19 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/TouchBistro/goutils/color" "github.com/TouchBistro/goutils/errors" @@ -45,6 +46,7 @@ type Config struct { Playlists map[string]playlist.Playlist `yaml:"playlists"` Overrides map[string]service.ServiceOverride `yaml:"overrides"` Registries []registry.Registry `yaml:"registries"` + TimeoutSeconds int `yaml:"timeoutSeconds"` } // NOTE: This is deprecated and is only here for backwards compatibility. @@ -151,6 +153,17 @@ func Init(ctx context.Context, config Config, opts InitOptions) (*engine.Engine, return nil, errors.New(errkind.Invalid, "no registries defined", op) } + + if config.TimeoutSeconds != 0 && (config.TimeoutSeconds < 5 || config.TimeoutSeconds > 3600) { + return nil, errors.New(errkind.Invalid, fmt.Sprintf("Invalid timeoutSeconds value '%d' in .tbrc.yaml. Values must be between 5 and 3600 inclusive", config.TimeoutSeconds), op) + } + + // default to 60 min timeout when not provided in .tbrc.yml + timeout := 60 * time.Minute + if config.TimeoutSeconds != 0 { + timeout = time.Duration(config.TimeoutSeconds) * time.Second + } + // Validate and normalize all registries. tracker := progress.TrackerFromContext(ctx) for i, r := range config.Registries { @@ -184,6 +197,7 @@ func Init(ctx context.Context, config Config, opts InitOptions) (*engine.Engine, err = progress.RunParallel(ctx, progress.RunParallelOptions{ Message: "Cloning/updating registries", Count: len(config.Registries), + Timeout: timeout, }, func(ctx context.Context, i int) error { r := config.Registries[i] if r.LocalPath != "" { @@ -293,6 +307,7 @@ func Init(ctx context.Context, config Config, opts InitOptions) (*engine.Engine, BaseImages: registryResult.BaseImages, LoginStrategies: registryResult.LoginStrategies, DeviceList: deviceList, + Timeout: timeout, }) if err != nil { return nil, errors.Wrap(err, errors.Meta{Reason: "failed to initialize engine", Op: op}) diff --git a/engine/app.go b/engine/app.go index 9566a3f..0420828 100644 --- a/engine/app.go +++ b/engine/app.go @@ -101,6 +101,7 @@ func (e *Engine) AppiOSRun(ctx context.Context, appName string, opts AppiOSRunOp // Download the app appPath, err := progress.RunT(ctx, progress.RunOptions{ Message: fmt.Sprintf("Downloading iOS app %s", a.FullName()), + Timeout: e.timeout, }, func(ctx context.Context) (string, error) { return e.downloadApp(ctx, a, app.TypeiOS, op) }) @@ -249,6 +250,7 @@ func (e *Engine) AppDesktopRun(ctx context.Context, appName string, opts AppDesk // Download the app appPath, err := progress.RunT(ctx, progress.RunOptions{ Message: fmt.Sprintf("Downloading Desktop app %s", a.FullName()), + Timeout: e.timeout, }, func(ctx context.Context) (string, error) { return e.downloadApp(ctx, a, app.TypeDesktop, op) }) diff --git a/engine/engine.go b/engine/engine.go index 6005235..84b7d1d 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -3,6 +3,7 @@ package engine import ( "os" "path/filepath" + "time" "github.com/TouchBistro/goutils/errors" "github.com/TouchBistro/tb/errkind" @@ -28,6 +29,7 @@ type Engine struct { loginStrategies []string deviceList simulator.DeviceList concurrency int + timeout time.Duration gitClient git.Git dockerClient *docker.Docker @@ -71,6 +73,9 @@ type Options struct { GitClient git.Git // DockerOptions is used to customize docker operations. DockerOptions docker.Options + // Timeout is a limit to how long an operation will last + // If no value is provided, it defaults to 3600 + Timeout time.Duration } // New creates a new Engine instance. @@ -113,6 +118,7 @@ func New(opts Options) (*Engine, error) { baseImages: opts.BaseImages, loginStrategies: opts.LoginStrategies, deviceList: opts.DeviceList, + timeout: opts.Timeout, concurrency: opts.Concurrency, gitClient: opts.GitClient, dockerClient: dockerClient, diff --git a/engine/service.go b/engine/service.go index 69877f4..f197849 100644 --- a/engine/service.go +++ b/engine/service.go @@ -91,6 +91,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { Message: "Logging into services", Count: len(loginStrategies), Concurrency: e.concurrency, + Timeout: e.timeout, // Bail if one fails since there's no point on waiting on the others // since we can't proceed anyway. CancelOnError: true, @@ -113,6 +114,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { // Cleanup previous docker state err = progress.Run(ctx, progress.RunOptions{ Message: "Cleaning up previous docker state", + Timeout: e.timeout, }, func(ctx context.Context) error { return e.stopServices(ctx, op, services) }) @@ -127,6 +129,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { Message: "Pulling docker base images", Count: len(e.baseImages), Concurrency: e.concurrency, + Timeout: e.timeout, }, func(ctx context.Context, i int) error { img := e.baseImages[i] if err := e.dockerClient.PullImage(ctx, img); err != nil { @@ -154,6 +157,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { Message: "Pulling docker service images", Count: len(images), Concurrency: e.concurrency, + Timeout: e.timeout, }, func(ctx context.Context, i int) error { img := images[i] if err := e.dockerClient.PullImage(ctx, img); err != nil { @@ -179,6 +183,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { if len(buildServices) > 0 { err := progress.Run(ctx, progress.RunOptions{ Message: "Building docker images for services", + Timeout: e.timeout, }, func(ctx context.Context) error { return e.dockerClient.BuildServices(ctx, buildServices) }) @@ -196,6 +201,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { err := progress.Run(ctx, progress.RunOptions{ Message: "Performing pre-run step for services (this may take a long time)", Count: len(services), + Timeout: e.timeout, }, func(ctx context.Context) error { for _, s := range services { if s.PreRun == "" { @@ -225,6 +231,7 @@ func (e *Engine) Up(ctx context.Context, opts UpOptions) error { // Start services err = progress.Run(ctx, progress.RunOptions{ Message: "Starting services in the background", + Timeout: e.timeout, }, func(ctx context.Context) error { return e.dockerClient.UpServices(ctx, getServiceNames(services)) }) @@ -250,6 +257,7 @@ func (e *Engine) Down(ctx context.Context, opts DownOptions) error { } err = progress.Run(ctx, progress.RunOptions{ Message: "Stopping services", + Timeout: e.timeout, }, func(ctx context.Context) error { return e.stopServices(ctx, op, services) }) @@ -411,6 +419,7 @@ func (e *Engine) Nuke(ctx context.Context, opts NukeOptions) error { const op = errors.Op("engine.Engine.Nuke") return progress.Run(ctx, progress.RunOptions{ Message: "Cleaning up tb data", + Timeout: e.timeout, }, func(ctx context.Context) error { return e.nuke(ctx, opts, op) }) @@ -661,6 +670,7 @@ func (e *Engine) prepareGitRepos(ctx context.Context, op errors.Op, skipPull boo Message: "Cloning/pulling service git repos", Count: len(actions), Concurrency: e.concurrency, + Timeout: e.timeout, }, func(ctx context.Context, i int) error { a := actions[i] if a.clone {