Devenv

A tool for running several services at once

Invocation

Basic invocation is this:

devenv target
where target is the name of the project you need to run. Devenv would run this project and all of it's dependencies in a detached screen session with the name devenv. Each of the projects would run in a separate window within this session. You can attach to that session and see how it's going, using, for example,
screen -dr
See screen documentation for more information.

You can destroy the session and kill all processes running within it with a simple command

devenv -k
or you can specify which project to kill, while letting others run:
devenv -k target1 target2...

You can run several projects (along with their dependencies) at once:

devenv target1 target2

If the screen session already exists, you can add projects to it with a -a switch:

devenv -a target3
That way devenv won't try to start a project that is already running.

You can also disable dependency tracking completely:

devenv -j target
In this case, only the projects (targets) listed in the command line would run.

It's possible to use a different session name:

devenv -s session_name target
This switch can be combined with
-k.

Configuration

devenv uses single configuration file, which lists all the projects with all steps necessary to run those. By default, this file is named .devenv.yml and is placed in the user's home directory. You can specify another configuration file with the -f switch:
devenv -f config_file.yml target
This switch can be combined with all other parameters listed above.

Configuration file uses YAML syntax. The reader is supposed to be familiar with YAML.

Configuration example

basedir: projects/main/src
init: setup_vpn -u current_user
targets:
  project_1:
    directory: pr1
    shell: run_project -z funny
    wait:
      port: 8080
    pause: 60
    init: init_projects -x or_not
    title: main
    error: err_pr_1
  project_2:
    directory: pr2
    dependencies:
      - project_1
      - project_3
    ...

Configuration syntax

The basedir parameter specifies the directory within which all projects reside. This can be an absolute or relative path. If it's relative, then the base directory for it is the directory in which the configuration file is found.

init is a shell command that should be run before all the projects. It's run in the same directory in which devenv is invoked.

targets map lists all projects that devenv can run. Each project corresponds to one entry in this map, with the name being the intended name of a project, and the value being another map, listing all the parameters of it.

The directory parameter is the directory in which the project should be running. It can be an absolute path, or a path relative to the basedir above.

shell is a shell command which runs the project. It is executed in the detached screen session. It's output is not shown, unless you attach to that session.

wait is a list of things devenv would be waiting for, while the project starts. Only when all of them are present, devenv would consider the project running. Currently the only thing that devenv can wait for is one port with arbitrary number, given in the port parameter.

After the project is successfully running, devenv would pause for the number of seconds, specified in the pause parameter. If that is not necessary, this parameter should be omitted.

After that timeout has passed, or, if it's absent, after the wait is over and the project is running, the command specified in the init parameter is run. It is not run in the screen session, and would normally communicate with the project in some other way.

Each project runs in it's own screen window within one session, and that window has a title given in the title parameter. If this parameter is omitted, the title of the window would be the same as the name of the project.

error is a file that devenv creates in the project directory and removes after the project finishes running. It helps determine if the project finished prematurely, or if it's already running. Omitting this parameter would result in devenv assuming no errors occured while starting the project, and that the project wasn't already running before.

devenv tracks dependencies between projects. The dependencies field is a list of other projects that should be run before this one. No projects would run if a circular dependency is detected, and an error would be reported.

Variables and overrides

devenv supports two features allowing to modify projects' behaviour.

Overrides

In the command line any number of override names can be listed using -o switch:

devenv -o override_1 -o override_2 target
Each time when devenv reads the value of any key in the configuration file, it checks if the map, in which this key is, has an overrides key. If such a key is present, and the corresponding value is a map, then it's elements are given a chance to override the value that is being fetched. Only the overrides with names listed in the command line are used. Those that do not provide a new value for that key are ignored. If there are several possible overrides, then the one with the name being the last in the command line is used.

Example:

...
    shell: run_project
    wait:
      port: 8080
    overrides:
      fast:
        shell: run_project --skip-slow-checks
      and:
        shell: run_project --extra-safe
        port: 8081
      furious:
        port: 8888
Normally, devenv would start the project using the command run_project, and wait for the port 8080 to become open. If, however, it is run as devenv -o fast, then it would use the command run_project --skip-slow-checks to start the project, but it would still wait for the port 8080. If it is run as devenv -o fast -o and -o furious, then the command run_project --extra-safe would be used, but devenv would wait for the port 8888 instead.

Overrides only apply to the same YAML map they are members of; they do not change anything in the subtrees below that map, or in other maps, not related to that one. Each map can contain it's own set of overrides. Not all overrides must be listed in the overrides map; those that are not listed are assumed to be empty.

There is no possible way to limit overrides to one particular project or other kind of map. All overrides specified in the command line are applied to everything in the configuration file.

Variables

All string parameters in the configuration file can use variables. The variable name is enclosed in curly braces: {var_name}. Each YAML map can provide a variables map, listing the values of all variables. Those values also should be strings.

Example:

...
    shell: run_project --listen-to {port_number}
    wait:
      port: {port_number}
    variables:
      port_number: 8080
In this example, command line run_project --listen-to 8080 would be used to start the project, and devenv would wait for the port 8080 to become open.

Unlike overrides, variables can be inherited: variable is not only substituted in any string that is in the same map, but also in the whole subtree under it. Variables specified on the deeper level take precedence:

targets:
  pr_1:
    shell: run_1 {arg}
    ...
  pr_2:
    shell: run_2 {arg}
    ...
  pr_3:
    shell: run_3 {arg}
    variables:
      arg: foo
    ...
  variables:
    arg: bar
In this example, targets pr_1 and pr_2 would be started with command lines run_1 bar and run_2 bar respectively, but the target pr_3 would use the command line run_3 foo instead.

Interaction between variables and overrides

Overrides can not contain variables or overrides on the first level. So, in case of
  ...
  overrides:
    name:
      foo: bar{baz}
      overrides:
        name:
          foo: quux
      variables
        baz: qaz
only the literal string bar{baz} would be used as an override for the key foo. Internal overrides and variables keys would be regarded as unrelated overrides with names overrides and variables.

On the other hand, there is nothing wrong with values of overrides containing overrides and variables of their own:

  ...
  overrides:
    foo:
      shell: run_1
      overrides:
        bar:
          shell: run_2
In this case, if an override name foo is given in the command line, command run_1 would be used, but if both foo and bar are given, command run_2 would be used instead. If bar is given in the command line, but foo is not, no overrides would be used, unless bar is mentioned elsewhere in the file.

Overrides also can refer to the variables that would be used even without override:

  ...
  shell: run_project {foo}
  overrides:
    quick:
      shell: run_project --quick {foo}
  variables
    foo: bar
If no override name is given, command line run_project bar would be used, but if override quick is in effect, it would be run_project --quick bar.

Variables, on the other hand, can't refer to other variables. Their values are substituted literally, with no further processing.

...
  shell: run {foo}
  variables:
    foo: {bar}
    bar: baz
In this case the command line would be run {bar}, with variable bar not being substituted.

Variables, however, can use overrides.

...
  shell: run {foo}
  overrides:
    bar:
      shell: run_bar {foo}
  variables
    foo: baz
    overrides:
      quux:
        foo: qaz
Normally, overrides would be ignored, and the command run baz used. If override bar is in effect, the command would be run_bar baz. If override quux is in effect, the command would be run qaz And, at last, if both overrides are listed on the command line, like devenv -o bar -o quux, then it would be run_bar qaz.

Links

Acknowledgments