diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ce3fb..a4a0518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Documentation.md b/Documentation.md index be7113e..2d8201f 100644 --- a/Documentation.md +++ b/Documentation.md @@ -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..." @@ -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 @@ -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 diff --git a/examples/moulti-man b/examples/moulti-man index 3c0be45..c50a900 100755 --- a/examples/moulti-man +++ b/examples/moulti-man @@ -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 "$@" diff --git a/examples/moulti-scoreboard.bash b/examples/moulti-scoreboard.bash index b4c631f..910d710 100755 --- a/examples/moulti-scoreboard.bash +++ b/examples/moulti-scoreboard.bash @@ -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: diff --git a/src/moulti/cli.py b/src/moulti/cli.py index 069edd4..509b261 100644 --- a/src/moulti/cli.py +++ b/src/moulti/cli.py @@ -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 @@ -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']: @@ -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