Skip to content

Commit

Permalink
moulti run: suffix the instance name to prevent conflicts.
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierog committed Jan 16, 2025
1 parent ffc8c07 commit 1fec286
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 13 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for its CLI (Command-Line Interface), i.e. for the `moulti` command.
Although Moulti's Python packages, modules and functions are obviously available, they do not constitute a public API yet.

## Unreleased

### Changed

- `moulti run` now suffixes the instance name with its process id (e.g. `default-1234` instead of `default`).
This helps prevent clashes and makes it possible to run various tools concurrently without having to worry about such details.
This behaviour can be prevented by setting the `MOULTI_RUN_NO_SUFFIX` environment variable or using the `-n` / `--no-suffix` command-line option.
The behaviour of `moulti init` remains unchanged.

## [1.29.0] - 2025-01-12

### Changed
Expand Down
20 changes: 11 additions & 9 deletions Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,23 @@ Limitation: shell completion will help typing subcommands and options but is tec
## Shell scripting

The key takeaway from the previous section is that Moulti starts as an empty shell that is meant to be controlled and filled through the `moulti` CLI tool.
A simple Moulti-based bash script makes use of two new commands:
Controlling a Moulti instance by typing individual commands is occasionally useful but, most of the time, Moulti is driven by a shell script.
To this end, a new subcommand proves helpful: `moulti run`.
`moulti run` is essentially the same as `moulti init` but it runs a command right after the startup phase.

- `moulti run`: same as `moulti init` but runs a command right after the startup phase
- `moulti wait`: tries connecting to the Moulti instance; think "ping for moulti"
That command inherits various environment variables; among them:
- `MOULTI_RUN`: if this variable is set to a non-empty value, the Moulti instance is already running and there is no need to launch it;
- `MOULTI_SOCKET_PATH`: this variable ensures all subsequent `moulti` commands connect to the right Moulti instance.

... and looks like this:
In practice, a simple Moulti-based bash script looks like this:

```bash
#!/usr/bin/env bash

# Name the Moulti instance to prevent conflicts:
# Good practice: name the Moulti instance:
export MOULTI_INSTANCE='my-first-script-with-moulti'
# If not done already, start a Moulti instance and have it re-execute this script:
[ "${MOULTI_RUN}" ] || exec moulti run -- "$0" "$@"
# Ensure the Moulti instance is reachable:
moulti wait
moulti step add step_1 --title='First step'
{
echo "$(date --iso-8601=ns) Starting combobulating things and stuff..."
Expand All @@ -142,7 +143,7 @@ moulti step add step_2 --title='Second step'
} 2>&1 | moulti pass step_2
```

In practice, it makes sense to heavily leverage shell functions, such as those available in [moulti-functions.bash](examples/moulti-functions.bash).
In real life, it makes sense to heavily leverage shell functions, such as those available in [moulti-functions.bash](examples/moulti-functions.bash).

### Dealing with existing scripts

Expand Down Expand Up @@ -930,7 +931,8 @@ Hit `n` to go to the next occurrence or `N` to go to the previous one.

#### Connectivity

- `MOULTI_INSTANCE`: name of your Moulti instance; defaults to `default`; taken into account by the instance to compute the default value of `MOULTI_SOCKET_PATH`.
- `MOULTI_INSTANCE`: name of your Moulti instance; defaults to `default`; `moulti init` keeps this value untouched; since v1.30.0, `moulti run` appends its process id (e.g. `default-1234`) to prevent conflicts; taken into account by the instance to compute the default value of `MOULTI_SOCKET_PATH`.
- `MOULTI_RUN_NO_SUFFIX`: if non-empty, instruct `moulti run` not to append its process id to the instance name; setting this environment variable is equivalent to running `moulti run --no-suffix`.
- `MOULTI_SOCKET_PATH`: path to the network socket that Moulti should listen/connect to; taken into account by both the instance and the CLI; if not specified, Moulti defaults to a platform-specific value that reflects your username and `MOULTI_INSTANCE`; examples: `@moulti-bob-test-instance.socket`, `/run/user/1000/moulti-bob-test-instance.socket`.
- `MOULTI_ALLOWED_UID`: abstract sockets (i.e. Linux) only! By default, Moulti only accepts commands from the user (uid) that launched the Moulti instance; setting this environment variable beforehand makes it accept arbitrary uids; example:
```shell
Expand Down
2 changes: 1 addition & 1 deletion examples/moulti-man
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ max_unindent=3
export MANWIDTH=$(((COLUMNS-moulti_columns+max_unindent)))

export MOULTI_INSTANCE="man-$$"
exec moulti run -- moulti manpage run -- man "$@"
exec moulti run --no-suffix -- moulti manpage run -- man "$@"
2 changes: 1 addition & 1 deletion examples/moulti-scoreboard.bash
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ if [ -z "${MOULTI_RUN}" ]; then
[ "${SCOREBOARD_COLUMNS}" ] && export COLUMNS="${SCOREBOARD_COLUMNS}"
[ "${SCOREBOARD_LINES}" ] && export LINES="${SCOREBOARD_LINES}"

exec moulti run -- "$0" "$@"
exec moulti run --no-suffix -- "$0" "$@"
fi

# Part 3: driver script:
Expand Down
23 changes: 21 additions & 2 deletions src/moulti/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from typing import Generator
import argcomplete
from . import __version__ as moulti_version
from .client import moulti_socket_path, send_to_moulti, send_to_moulti_and_handle_reply, pipeline
from .environ import pint, float_str
from .client import current_instance, moulti_socket_path, send_to_moulti, send_to_moulti_and_handle_reply, pipeline
from .environ import env, pint, float_str
from .widgets.cli import add_cli_arguments
from .manpage import manpage_parse, manpage_run

Expand All @@ -18,8 +18,26 @@ def init(args: dict) -> None:
from .app import main as init_moulti # pylint: disable=import-outside-toplevel
init_moulti(**args)

def moulti_run_should_suffix_instance_name(args: dict) -> bool:
"""
By default, `moulti run` suffixes the instance name with the process id.
Since the socket path is derived from the instance name, this helps prevent clashes and thus
"cannot listen" errors.
"""
if env('MOULTI_SOCKET_PATH'):
# If a socket path was explicitly set, then the instance name cannot affect
# its computation. Therefore, there is no need to suffix the instance name.
return False
if args['no_suffix']:
return False
if env('MOULTI_RUN_NO_SUFFIX') is not None:
return False
return True

def run(args: dict) -> None:
"""Start a new Moulti instance and run the given command."""
if moulti_run_should_suffix_instance_name(args):
os.environ['MOULTI_INSTANCE'] = f'{current_instance()}-{os.getpid()}'

# Handle --print-env:
if args['print_env']:
Expand Down Expand Up @@ -96,6 +114,7 @@ def add_main_commands(subparsers: _SubParsersAction) -> None:
run_parser = subparsers.add_parser('run', help='start a new Moulti instance and run a command')
run_parser.set_defaults(func=run)
run_parser.add_argument('--print-env', action='store_true', default=False, help='print environment variables set by Moulti and exit')
run_parser.add_argument('--no-suffix', '-n', action='store_true', default=False, help='do not suffix the instance name with the process id')
run_parser.add_argument('command', type=str, nargs='+', help='command to run along with its arguments')

# moulti wait
Expand Down

0 comments on commit 1fec286

Please sign in to comment.