Skip to content

Commit

Permalink
feat: Add timeouts to commands.
Browse files Browse the repository at this point in the history
  • Loading branch information
jwalton committed Apr 16, 2022
1 parent 224968a commit f78c9a4
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 64 deletions.
2 changes: 1 addition & 1 deletion cmd/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ var promptCmd = &cobra.Command{
context = modules.NewDemoContext(*demoConfig, styles)
} else {
globals := modules.NewGlobals(shell, cwd, logicalCWD, terminalWidth, status, jobs, cmdDuration, keymap)
context = modules.NewContext(globals, configuration.ProjectsTypes, cacheDir, styles)
context = modules.NewContext(globals, configuration.ProjectsTypes, configuration.Timeout, cacheDir, styles)
}
performance.End("Context setup")

Expand Down
27 changes: 27 additions & 0 deletions docs/docs/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
sidebar_position: 1
---

# Configuration

The top level of the configuration file may contain the following keys:

## timeout

This specified the default timeout for modules where the timeout is unspecified. Note that block modules ignore this - the default timeout for a block module is infinite. A value of 0 here will be an infinite timeout. If not specified, this defaults to 500ms.

## extends

The name of another configuration file to extend (the parent configuration file). We load colors, prompt, and projects from the parent file first, then merge in any custom colors or project configuration from the current file. See [Configuration Merging](../configurationMerging.mdx).

## colors

A map of custom colors. Custom colors must start with a "$". See [Styles](../styles.mdx).

## projectTypes

An array of project types. See [Projects](../projects.mdx).

## prompt

The [module](./modules.mdx) to render as the prompt. Typically this would be a block module with multiple child modules.
2 changes: 1 addition & 1 deletion docs/docs/reference/functions.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
---

# Template Functions
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/globals.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 3
---

# Template Globals
Expand Down
18 changes: 10 additions & 8 deletions docs/docs/reference/modules.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 2
---

# Modules
Expand All @@ -10,6 +10,9 @@ There are certain configuration items that are available on all modules:

- `style` a the [style string](/docs/styles) to apply to the entire module output.
- `template` is a golang template used to render the result of the module.
- `timeout` is the maximum amount of time the module is allowed to run, in milliseconds.

If the timeout of a block is exceeded, the module's output will be empty, and the template for the module will not be run. If you're using a template in a parent block, note especially that the module's `.Data` will be empty, too. If `timeout` is unspecified, then the default timeout will be set to the `timeout` value specified at the top-level of the config file, or 500ms if unspecified. Blocks are treated specially here - a block's default timeout is infinite.

If a module is a child of a "block" module, it can also have the following items:

Expand Down Expand Up @@ -49,7 +52,7 @@ Configuration:

Outputs:

- `Modules` is a map of results from executing each child module. The keys of this map are module IDs (or module types, for modules that have no ID). Only modules that actually generated output will be included. If a module does not have an ID, then the module's `type` will be used to index the module results. The values in this map are `{Text, Data, StartStyle, EndStyle}` objects, where `Text` is the default output from the module, `Data` is the output variables from the module, and `StartStyle` and `EndStyle` are each a `{FG, BG}` object containing the style of the first and last character of that module - these are based entirely on the module's declared `Style`, so if the module uses a template to style part of the string, these won't be reflected in FG and BG.
- `Modules` is a map of results from executing each child module. The keys of this map are module IDs (or module types, for modules that have no ID). If a module does not have an ID, then the module's `type` will be used to index the module results. The values in this map are `{Text, Data, StartStyle, EndStyle}` objects, where `Text` is the default output from the module, `Data` is the output variables from the module, and `StartStyle` and `EndStyle` are each a `{FG, BG}` object containing the style of the first and last character of that module - these are based entirely on the module's declared `Style`, so if the module uses a template to style part of the string, these won't be reflected in FG and BG. Modules are always included in this map, even if they produced no output, but note that if a module times out, then `Modules[id].Data` will be an empty object.
- `ModuleArray` is an array of results from executing each child module. Only modules that actually generated output will be included.

## custom
Expand Down Expand Up @@ -136,7 +139,7 @@ Outputs:

## flexible_space

The flexible_space module adds a variable-width space to a line. The space will grow to use as many characters as possible without causing the current line to wrap. This can be used to split a prompt into a portion printed on the left side of the terminal and a second portion printed on the right side. You can put multiple flexible_spaces on a single line, in which case the available space will be split evenly between them (you could use two flexible_spaces to center some text, for example).
The flexible_space module adds a variable-width space to a line. The space will grow to use as many characters as possible without causing the current line to wrap. This can be used to split a prompt into a portion printed on the left side of the terminal and a second portion printed on the right side. You can put multiple flexible_spaces on a single line, in which case the available space will be split evenly between them (you could use two flexible_spaces to center some text, for example).

## git_head

Expand All @@ -156,7 +159,7 @@ Outputs:

## git_state

The git_state module returns the state of the current git repo. For example, if you are in the middle of an interactive rebase, and you're on the second commit of four, this will return "REBASE-i 2/4". If the current folder is not a git repo, or if we're not in the middle of a rebase, merge, etc..., this will return the empty string. The default configuration is based on [posh-git](https://github.com/dahlbyk/posh-git) and [posh-git-sh](https://github.com/lyze/posh-git-sh).
The git_state module returns the state of the current git repo. For example, if you are in the middle of an interactive rebase, and you're on the second commit of four, this will return "REBASE-i 2/4". If the current folder is not a git repo, or if we're not in the middle of a rebase, merge, etc..., this will return the empty string. The default configuration is based on [posh-git](https://github.com/dahlbyk/posh-git) and [posh-git-sh](https://github.com/lyze/posh-git-sh).

Configuration:

Expand Down Expand Up @@ -216,11 +219,10 @@ Output Example:
"Unstaged": { "Added": 0, "Modified": 0, "Deleted": 0, "Total": 0 },
"Index": { "Added": 0, "Modified": 0, "Deleted": 0, "Total": 0 },
"Unmerged": 0,
"StashCount": 0,
"StashCount": 0
}
```


## hostname

The hostname module shows the current hostname. By default, this will only display anything if the user is currently logged in via SSH.
Expand Down Expand Up @@ -257,12 +259,12 @@ The project module works out what kind of project the current folder represents,

The default output of the project module will be something like "w/nodejs@16.13.2" or "w/go@1.17.5".

Each project has a unique style associated with it; the style comes from the `style` field in the `projects` map in this module. If none is specified, then it falls back to the `defaultProjectStyle` in this module. If that is also unspecified, then the style will be taken from teh top-level `projects` configuration.
Each project has a unique style associated with it; the style comes from the `style` field in the `projects` map in this module. If none is specified, then it falls back to the `defaultProjectStyle` in this module. If that is also unspecified, then the style will be taken from teh top-level `projects` configuration.

Configuration:

- `projects` is a map where keys are project names, and values are `{ style, toolSymbol, packageManagerSymbol }` objects, which can be used to provide a custom style and symbols for existing projects on a theme-by-theme basis.
- `defaultProjectStyle` is the style to use if no project-specific style is specified in `projects`. If this is also unspecified, we will fall back to the style specified in the top level `projects` configuration.
- `defaultProjectStyle` is the style to use if no project-specific style is specified in `projects`. If this is also unspecified, we will fall back to the style specified in the top level `projects` configuration.

Outputs:

Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jwalton/gchalk v1.2.1 h1:4OsqFRvg/fQq+hpqhDB180XYNqgYGtr75y0a+hoYy1U=
github.com/jwalton/gchalk v1.2.1/go.mod h1:ytRlj60R9f7r53IAElbpq4lVuPOPNg2J4tJcCxtFqr8=
github.com/jwalton/go-ansiparser v0.4.0 h1:1n0Yz4HAwRsd4rpJXx6ZhByTHqQ+2brRPkeV5P8zyzA=
github.com/jwalton/go-ansiparser v0.4.0/go.mod h1:q/bzAdZmLhXOwJcdgaYeJw5M5baCDgwocVnNd+DVH18=
github.com/jwalton/go-ansiparser v0.5.1 h1:6Fi+SoIwFcLOyUHpjRdupJ1pgV0PVjxxonVFAshG8Q0=
github.com/jwalton/go-ansiparser v0.5.1/go.mod h1:S8tCBAG8h82B8Lsmqd0wRrrRD10wsJH5v4WuC23GGdQ=
github.com/jwalton/go-supportscolor v1.1.0 h1:HsXFJdMPjRUAx8cIW6g30hVSFYaxh9yRQwEWgkAR7lQ=
Expand Down Expand Up @@ -245,13 +243,11 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
53 changes: 29 additions & 24 deletions internal/gitutils/caching.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package gitutils

import "sync"

// caching is a gitutils that caches results - it assumes the underlying repo
// is not going to change between calls.
type caching struct {
// TOOD: Make this thread-safe?
underlying Git

stashCountInitialized bool
stashCount int
mutex sync.Mutex

stashCountOnce sync.Once
stashCount int
stashCountErr error

localBranch string
upstreamBranch string
Expand All @@ -19,8 +23,11 @@ type caching struct {

headInfoTagsSearched int
headInfo *HeadInfo
stateOnce sync.Once
state *RepositoryState
stats *GitStats
statsOnce sync.Once
stats GitStats
statsError error
}

// NewCaching returns a new caching instance of Git. The returned instance
Expand All @@ -41,20 +48,17 @@ func (c *caching) RepoRoot() string {

// GetStashCount returns the number of stashes.
func (c *caching) GetStashCount() (int, error) {
if !c.stashCountInitialized {
var err error
c.stashCount, err = c.underlying.GetStashCount()
if err != nil {
return 0, err
}
c.stashCountInitialized = true
}
return c.stashCount, nil
c.stashCountOnce.Do(func() {
c.stashCount, c.stashCountErr = c.underlying.GetStashCount()
})
return c.stashCount, c.stashCountErr
}

// GetUpstream returns the upstream of the current branch if one exists, or
// an empty string otherwise.
func (c *caching) GetUpstream(branch string) string {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.localBranch != branch {
c.localBranch = branch
c.upstreamBranch = c.underlying.GetUpstream(branch)
Expand All @@ -65,6 +69,9 @@ func (c *caching) GetUpstream(branch string) string {
// GetAheadBehind returns how many commits ahead and behind the given
// localRef is compared to remoteRef.
func (c *caching) GetAheadBehind(localRef string, remoteRef string) (ahead int, behind int, err error) {
c.mutex.Lock()
defer c.mutex.Unlock()

if c.aheadBehindLocalRef != localRef || c.aheadBehindRemoteRef != remoteRef {
ahead, behind, err = c.underlying.GetAheadBehind(localRef, remoteRef)
if err != nil {
Expand All @@ -80,6 +87,9 @@ func (c *caching) GetAheadBehind(localRef string, remoteRef string) (ahead int,

// Head returns information about the current head.
func (c *caching) Head(maxTagsToSearch int) (head HeadInfo, err error) {
c.mutex.Lock()
defer c.mutex.Unlock()

haveHeadInfo := c.headInfo != nil && (!c.headInfo.Detached || c.headInfo.IsTag || maxTagsToSearch < c.headInfoTagsSearched)
if !haveHeadInfo {
c.headInfoTagsSearched = maxTagsToSearch
Expand All @@ -94,22 +104,17 @@ func (c *caching) Head(maxTagsToSearch int) (head HeadInfo, err error) {

// State returns the current state of the repository.
func (c *caching) State() RepositoryState {
if c.state == nil {
c.stateOnce.Do(func() {
state := c.underlying.State()
c.state = &state
}
})
return *c.state
}

// Stats returns status counters for the given git repo.
func (c *caching) Stats() (GitStats, error) {
if c.stats == nil {
stats, err := c.underlying.Stats()
if err != nil {
return GitStats{}, err
}
c.stats = &stats
}

return *c.stats, nil
c.statsOnce.Do(func() {
c.stats, c.statsError = c.underlying.Stats()
})
return c.stats, c.statsError
}
13 changes: 10 additions & 3 deletions internal/kitsch/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import (
"gopkg.in/yaml.v3"
)

const defaultTimeout = 500

var errNoPrompt = errors.New("configuration is missing prompt")

// Config represents a configuration file.
type Config struct {
// Timeout is the default module timeout, in milliseconds.
Timeout int64 `yaml:"timeout"`
// Extends is the name of another configuration file to extend.
Extends string `yaml:"extends"`
// Colors is a collection of custom colors.
Expand All @@ -31,6 +35,10 @@ type Config struct {
Prompt modules.ModuleWrapper
}

func newConfig() Config {
return Config{Timeout: defaultTimeout}
}

// LoadFromYaml loads the configuration file from a YAML file.
func (c *Config) LoadFromYaml(yamlData []byte, strict bool) error {
decoder := yaml.NewDecoder(bytes.NewReader(yamlData))
Expand Down Expand Up @@ -80,8 +88,7 @@ func (c *Config) mergeParent(parent *Config) {

// LoadConfigFromFile will load a configuration from a file.
func LoadConfigFromFile(configFile string, strict bool) (*Config, error) {
var config = Config{}

var config = newConfig()
yamlData, err := os.ReadFile(configFile)
if err != nil {
return nil, err
Expand All @@ -101,7 +108,7 @@ func LoadConfigFromFile(configFile string, strict bool) (*Config, error) {

// LoadDefaultConfig will load a default configuration.
func LoadDefaultConfig() (*Config, error) {
var config = Config{}
var config = newConfig()
err := config.LoadFromYaml(sampleconfig.DefaultConfig, false)
if err != nil {
// Default config should not have errors!
Expand Down
2 changes: 1 addition & 1 deletion internal/kitsch/getters/customGetter.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,5 +418,5 @@ func (getter CustomGetter) applyTemplate(as AsType, bytesValue []byte) (interfac
return result, nil
}

//JSONSchemaDefinitions is a string containing JSON schema definitions for objects in the getters package.
// JSONSchemaDefinitions is a string containing JSON schema definitions for objects in the getters package.
var JSONSchemaDefinitions = "\"CacheSettings\": " + cacheSettingsJSONSchema + ",\n\"CustomGetter\": " + customGetterJSONSchema
6 changes: 5 additions & 1 deletion internal/kitsch/getters/resolveFile_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package getters

import (
"path/filepath"
"strings"
"testing"
"testing/fstest"

Expand All @@ -15,5 +17,7 @@ func TestResolveFile(t *testing.T) {
}

result := resolveFile(context, "~/${VAR1}/$VAR2/baz")
assert.Equal(t, "/users/jwalton/foo/bar/baz", result)
expected := strings.Replace("/users/jwalton/foo/bar/baz", "/", string(filepath.Separator), -1)

assert.Equal(t, expected, result)
}
16 changes: 16 additions & 0 deletions internal/kitsch/modules/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ func TestBlockSubIDs(t *testing.T) {

assert.Equal(t, "oriana", result.Text)
}

func TestBlockSubIDsNoData(t *testing.T) {
blockMod := moduleWrapperFromYAML(heredoc.Doc(`
type: block
template: "{{ with .Data.Modules.text }}there is text{{ end }}"
modules:
- type: text
text: ""
`))

result := blockMod.Execute(newTestContext("oriana"))

// This should render text, because the "text" module should be included
// in .Data.Modules, even though there was no output.
assert.Equal(t, "there is text", result.Text)
}
4 changes: 4 additions & 0 deletions internal/kitsch/modules/commonConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type CommonConfig struct {
Template string `yaml:"template"`
// Conditions are conditions that must be met for this module to execute.
Conditions *condition.Conditions `yaml:"conditions,omitempty" jsonschema:",ref"`
// Timeout is the maximum amount of time, in milliseconds, to wait for this
// module to execute. If not specified, the default timeout for most modules
// will be 200ms, but for block modules it will be infinite.
Timeout int64 `yaml:"timeout"`
}

// Validate checks for common configuration errors in the CommonConfig, and prints
Expand Down
17 changes: 11 additions & 6 deletions internal/kitsch/modules/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ type Context struct {
ValueCache cache.Cache
// Styles is the style registry to use to create styles.
Styles styling.Registry
// DefaultTimeout is the default module timeout, in milliseconds.
DefaultTimeout int64

mutex sync.Mutex
gitInitialized bool
Expand Down Expand Up @@ -182,16 +184,18 @@ func (context *Context) GetStyle(styleString string) *styling.Style {
func NewContext(
globals Globals,
projectTypes []projects.ProjectType,
defaultTimeout int64,
cacheDir string,
styles styling.Registry,
) Context {
return Context{
Globals: globals,
Directory: fileutils.NewDirectory(globals.CWD),
Environment: env.New(),
ProjectTypes: projectTypes,
ValueCache: cache.NewFileCache(cacheDir),
Styles: styles,
Globals: globals,
Directory: fileutils.NewDirectory(globals.CWD),
Environment: env.New(),
ProjectTypes: projectTypes,
ValueCache: cache.NewFileCache(cacheDir),
Styles: styles,
DefaultTimeout: defaultTimeout,
}
}

Expand Down Expand Up @@ -260,6 +264,7 @@ func NewDemoContext(
Styles: styles,
gitInitialized: true,
git: config.Git,
DefaultTimeout: 1000,
}
}

Expand Down
Loading

0 comments on commit f78c9a4

Please sign in to comment.