From d5a9d839907656be43a226d63d92264d3afdbac2 Mon Sep 17 00:00:00 2001 From: Steevan BARBOYON Date: Sun, 16 Nov 2025 20:18:23 +0100 Subject: [PATCH] Create readthedocs documentation --- .readthedocs.yml | 19 ---- docs/.readthedocs.yaml | 19 ++++ docs/assets/css/theme.css | 44 ++++++++ docs/assets/images/demo.gif | Bin 0 -> 63534 bytes docs/assets/js/readthedocs-search.js | 6 + docs/assets/js/try-it.js | 11 ++ docs/contributing/tips-for-devs.md | 33 ++++++ docs/examples/available-examples.md | 28 +++++ docs/getting-started/license.md | 11 ++ docs/index.md | 76 +++++++++++++ docs/installation/composer-dependency.md | 33 ++++++ docs/installation/docker-image.md | 106 ++++++++++++++++++ docs/mkdocs.yml | 57 ++++++++++ docs/requirements.txt | 3 + docs/theme/configuration.md | 68 +++++++++++ docs/theme/create-a-theme.md | 59 ++++++++++ docs/usage/application-configuration.md | 57 ++++++++++ docs/usage/bootstrap-tear-down.md | 76 +++++++++++++ docs/usage/minimal-configuration.md | 36 ++++++ docs/usage/output-verbosity.md | 42 +++++++ .../process-configuration/miscellaneous.md | 46 ++++++++ .../process-configuration/output-prefix.md | 76 +++++++++++++ docs/usage/process-configuration/verbosity.md | 77 +++++++++++++ examples/demo.php | 42 +++++++ examples/parallel-processes-example.sh | 26 +++++ .../canceled-as-error.php | 35 ++++++ .../process-configuration/output-prefix.php | 27 +++++ .../output-state-prefix.php | 27 +++++ .../output-summary-prefix.php | 27 +++++ 29 files changed, 1148 insertions(+), 19 deletions(-) delete mode 100644 .readthedocs.yml create mode 100644 docs/.readthedocs.yaml create mode 100644 docs/assets/css/theme.css create mode 100644 docs/assets/images/demo.gif create mode 100644 docs/assets/js/readthedocs-search.js create mode 100644 docs/assets/js/try-it.js create mode 100644 docs/contributing/tips-for-devs.md create mode 100644 docs/examples/available-examples.md create mode 100644 docs/getting-started/license.md create mode 100644 docs/index.md create mode 100644 docs/installation/composer-dependency.md create mode 100644 docs/installation/docker-image.md create mode 100644 docs/mkdocs.yml create mode 100644 docs/requirements.txt create mode 100644 docs/theme/configuration.md create mode 100644 docs/theme/create-a-theme.md create mode 100644 docs/usage/application-configuration.md create mode 100644 docs/usage/bootstrap-tear-down.md create mode 100644 docs/usage/minimal-configuration.md create mode 100644 docs/usage/output-verbosity.md create mode 100644 docs/usage/process-configuration/miscellaneous.md create mode 100644 docs/usage/process-configuration/output-prefix.md create mode 100644 docs/usage/process-configuration/verbosity.md create mode 100644 examples/demo.php create mode 100755 examples/parallel-processes-example.sh create mode 100644 examples/usage/process-configuration/canceled-as-error.php create mode 100644 examples/usage/process-configuration/output-prefix.php create mode 100644 examples/usage/process-configuration/output-state-prefix.php create mode 100644 examples/usage/process-configuration/output-summary-prefix.php diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 89aed7f..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 - -build: - os: ubuntu-24.04 - tools: - python: "3.13" - -sphinx: - configuration: docs/conf.py - - # Optionally, but recommended, - # declare the Python requirements required to build your documentation - # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html - # python: - # install: - # - requirements: docs/requirements.txt diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 0000000..cc127a2 --- /dev/null +++ b/docs/.readthedocs.yaml @@ -0,0 +1,19 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + jobs: + pre_install: + - pip install mkdocs-material + +mkdocs: + configuration: docs/mkdocs.yml + +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/assets/css/theme.css b/docs/assets/css/theme.css new file mode 100644 index 0000000..c6e012f --- /dev/null +++ b/docs/assets/css/theme.css @@ -0,0 +1,44 @@ +body { + cursor: default; +} + +h1 { + margin-bottom: 0.5em !important; + color: #4051B5 !important; + font-weight: 500 !important; +} + +h2 { + margin-top: 0.5em !important; + margin-bottom: 0.5em !important; + color: #4051B5 !important; + font-size: 1.3em; +} + +div.md-main__inner { + margin-top: 0; +} + +:root { + --md-typeset-a-color: #30a2a9; +} + +.icon-green { + color: #069006; +} + +.md-typeset .admonition > .admonition-title { + display: flex; + justify-content: space-between; + align-items: center; +} + +.tryit-link { + margin-left: auto; +} + +li.md-nav__item--active > label.md-nav__link > span.md-ellipsis, +li.md-nav__item--active > label.md-nav__link > span.md-nav__icon +{ + color: var(--md-typeset-a-color); +} diff --git a/docs/assets/images/demo.gif b/docs/assets/images/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ec6ee6d0cb59db70c08a95f5ea7f3550bb867f22 GIT binary patch literal 63534 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 Ts3d_U3~{k@k`Ochu=)Q0YV@IT literal 0 HcmV?d00001 diff --git a/docs/assets/js/readthedocs-search.js b/docs/assets/js/readthedocs-search.js new file mode 100644 index 0000000..432bd90 --- /dev/null +++ b/docs/assets/js/readthedocs-search.js @@ -0,0 +1,6 @@ +document.addEventListener("DOMContentLoaded", function(event) { + document.querySelector(".md-search__input").addEventListener("focus", (e) => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); +}); diff --git a/docs/assets/js/try-it.js b/docs/assets/js/try-it.js new file mode 100644 index 0000000..93d8ee5 --- /dev/null +++ b/docs/assets/js/try-it.js @@ -0,0 +1,11 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll(".admonition .admonition-title").forEach(title => { + if (title.textContent.trim() === "Try it!") { + const link = document.createElement("a"); + link.href = "/en/latest/examples/available-examples"; + link.className = "tryit-link"; + link.textContent = "How to execute examples"; + title.appendChild(link); + } + }); +}); diff --git a/docs/contributing/tips-for-devs.md b/docs/contributing/tips-for-devs.md new file mode 100644 index 0000000..6229195 --- /dev/null +++ b/docs/contributing/tips-for-devs.md @@ -0,0 +1,33 @@ +--- +hide: + - toc +--- + +# Work on the code + +You can clone the project: +```bash +git clone git@github.com:steevanb/php-parallel-processes.git +``` + +Then, when you to execute your local code, you need to add a volume on `/composer/vendor/steevanb/php-parallel-processes`: +```bash hl_lines="6" +docker \ + run \ + --rm \ + -it \ + -v "$(pwd)":/app \ + -v "$(pwd)":/composer/vendor/steevanb/php-parallel-processes \ + steevanb/php-parallel-processes:{{ package_version }}-alpine \ + php /app/parallel-processes.php +``` + +# Work on the documentation + +We use [readthedocs](https://about.readthedocs.com/), +[mkdocs](https://docs.readthedocs.com/platform/stable/intro/mkdocs.html) +and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/reference). + +Documentation is written in Markdown in [docs/](https://github.com/steevanb/php-parallel-processes/tree/readthedocs/docs). + +See [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/reference/code-blocks/) for Markdown syntaxes and examples. diff --git a/docs/examples/available-examples.md b/docs/examples/available-examples.md new file mode 100644 index 0000000..25635a6 --- /dev/null +++ b/docs/examples/available-examples.md @@ -0,0 +1,28 @@ +--- +hide: + - toc +--- + +# Available examples + +All examples are in [examples/](https://github.com/steevanb/php-parallel-processes/tree/master/examples). + +# How to execute an example + +You need to download the latest version of [parallel-processes-example.sh](https://raw.githubusercontent.com/steevanb/php-parallel-processes/refs/heads/master/examples/parallel-processes-example.sh). + +`parallel-processes-example.sh` has one mandatory argument: the path to the example, after `examples/` directory: +```bash +parallel-processes-example.sh path/to/example.php +``` + +# Configure an example + +Some of them can be configured with env vars. + +All configuration are prefixed by `PP_`. + +```bash hl_lines="1" +PP_CANCELED_AS_ERROR=true \ + parallel-processes-example.sh usage/process-configuration/canceled-as-error.php +``` diff --git a/docs/getting-started/license.md b/docs/getting-started/license.md new file mode 100644 index 0000000..daaabc9 --- /dev/null +++ b/docs/getting-started/license.md @@ -0,0 +1,11 @@ +--- +hide: + - toc +--- + +# License + +`parallel-processes` is licensed under the [Apache 2.0 License](https://github.com/steevanb/php-parallel-processes/blob/master/LICENSE). + +The Apache 2.0 license allows you to freely use, modify, and distribute software, including for commercial purposes, +as long as you keep the copyright and license notices and indicate any changes made. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..87786bd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,76 @@ +--- +hide: + - toc +--- + +# Presentation + +`parallel-processes` is an open-source PHP library for running multiple processes in parallel. + +![Parallel Processes demo](assets/images/demo.gif) + +# Key features + +

+ +- :material-docker:{ .lg .middle } __Install it or use Docker images__ + + --- + + Install it as a Composer dependency or use the provided Docker images to avoid a local installation. + + [:octicons-arrow-right-24: Installation](installation/composer-dependency.md) + +- :material-file-tree:{ .lg .middle } __Process start rules__ + + --- + + Configure when a process should start: immediately, after another process, during bootstrap, or during teardown. + + [:octicons-arrow-right-24: Bootstrap or tear down](usage/bootstrap-tear-down.md) + +- :material-cog-outline:{ .lg .middle } __Process configuration__ + + --- + + Configure each process: name, command, maximum execution time, output verbosity, etc. + + [:octicons-arrow-right-24: Configure process](usage/process-configuration/miscellaneous.md) + +- :material-cog-outline:{ .lg .middle } __Execution configuration__ + + --- + + Configure execution settings for your processes using PHP: process order, refresh interval, timeout, etc. + + [:octicons-arrow-right-24: Minimal configuration](usage/minimal-configuration.md) + +- :material-message-bulleted:{ .lg .middle } __Verbosity__ + + --- + + Control output verbosity: show process output, execution time, only errors, or all outputs. + + [:octicons-arrow-right-24: Output verbosity](usage/output-verbosity.md) + +- :material-compare:{ .lg .middle } __Theme__ + + --- + + Choose between the Default and Summary themes, or create a custom theme. + + [:octicons-arrow-right-24: Theme configuration](theme/configuration.md) + +
+ +# License + +`parallel-processes` is licensed under the [Apache 2.0 License](getting-started/license.md). + +# Links + +GitHub: [steevanb/php-parallel-processes](https://github.com/steevanb/php-parallel-processes) + +Docker Hub: [steevanb/php-parallel-processes](https://hub.docker.com/r/steevanb/php-parallel-processes) + +Creator: [Steevan BARBOYON](http://prestation.info-droid.fr/) diff --git a/docs/installation/composer-dependency.md b/docs/installation/composer-dependency.md new file mode 100644 index 0000000..c41659f --- /dev/null +++ b/docs/installation/composer-dependency.md @@ -0,0 +1,33 @@ +--- +hide: + - toc +--- + +# Composer dependency + +If you want to install `parallel-processses` in your project, with Composer: + +```bash +composer require steevanb/php-parallel-processes:^{{ package_version }} +``` + +# Dependencies + +
+ +- :material-penguin: __OS__: Linux and macOS, no tested on Windows +- :material-language-php: __PHP__: ^8.2 +- :material-symfony: __[symfony/console](https://symfony.com/doc/current/components/console.html)__: ^7.0 +- :material-symfony: __[symfony/process](https://symfony.com/doc/current/components/process.html)__: ^7.0 + +
+ +# Configure your processes + +See [minimal configuration](../usage/minimal-configuration.md). + +# Available versions + +See [releases](https://github.com/steevanb/php-parallel-processes/releases). + +See [changelog](https://github.com/steevanb/php-parallel-processes/blob/master/changelog.md). diff --git a/docs/installation/docker-image.md b/docs/installation/docker-image.md new file mode 100644 index 0000000..83c5e0c --- /dev/null +++ b/docs/installation/docker-image.md @@ -0,0 +1,106 @@ +--- +hide: + - toc +--- + +# Use Docker image + +`parallel-processses` provide Docker images with everything installed: PHP, the library and Docker. + +You can choose between this 3 versions: + +* `alpine`: smallest version, but could be "too much simple" sometimes +* `buster`: middle version, contains almost everything needed +* `bookworm`: larger version, should contain what you need + +# Dependencies + +
+ +- :material-penguin: __OS__: Linux, Windows and macOS +- :material-docker: __Docker__: you should already have a compatible version + +
+ +# Basic usage + +See [minimal configuration](../usage/minimal-configuration.md) to create your parallel processes configuration. + +Then you can use official images to execute it: +```bash +docker \ + run \ + --rm \ + -it \ + -v "$(pwd)":/app \ + steevanb/php-parallel-processes:{{ package_version }}-alpine \ + php /app/parallel-processes.php +``` + +# Docker outside of Docker (DooD) + +If your processes need to execute Docker commands on your host, you can do it with DooD. + +You have to add a volume to your Docker socket, that's all: + +```bash hl_lines="6" +docker \ + run \ + --rm \ + -it \ + -v "$(pwd)":/app \ + -v /var/run/docker.sock:/var/run/docker.sock \ + steevanb/php-parallel-processes:{{ package_version }}-alpine \ + php /app/parallel-processes.php +``` + +!!! info "docker.sock host path" + Use `docker context inspect` to get your socket path if it's not `/var/run/docker.sock`. + + In this case, keep `/var/run/docker.sock` for the volume target. + +Examples of processes that use the Docker host's socket: +```php +addProcess(new Process(['docker', 'build'])) + + // You can use Docker compose plugin + ->addProcess(new Process(['docker', 'compose', 'build'])) + + // You can use Docker buildx plugin, but only with buster and bookworm images + // alpine do not contain buildx + ->addProcess(new Process(['docker', 'buildx', 'build'])) + + ->run(new ArgvInput($argv)); +``` + +Official images are bundled with Docker and some plugins: + +| Image | docker | compose plugin | buildx plugin | +|-------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:-----------------------------------------:|:-----------------------------------------:| +| [{{ package_version }}-alpine](https://hub.docker.com/r/steevanb/php-parallel-processes/tags?name={{ package_version }}-alpine) | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | | +| [{{ package_version }}-buster](https://hub.docker.com/r/steevanb/php-parallel-processes/tags?name={{ package_version }}-buster) | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | +| [{{ package_version }}-bookworm](https://hub.docker.com/r/steevanb/php-parallel-processes/tags?name={{ package_version }}-bookworm) | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | + +!!! info "Docker is installed since 1.1.0" + Docker, compose and buildx plugins are installed in Docker images since `1.1.0`. + +# Available versions + +See [Docker tags](https://hub.docker.com/r/steevanb/php-parallel-processes/tags). + +See [changelog](https://github.com/steevanb/php-parallel-processes/blob/master/changelog.md). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..2e1ce9d --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,57 @@ +site_name: Parallel processes +theme: + name: material + features: + - content.code.copy +site_url: !ENV READTHEDOCS_CANONICAL_URL +extra_javascript: + - assets/js/readthedocs-search.js + - assets/js/try-it.js +extra_css: + - assets/css/theme.css + - assets/css/try-it.css +docs_dir: . +nav: + - Getting started: + - Presentation: index.md + - License: getting-started/license.md + - Installation: + - Composer dependency: installation/composer-dependency.md + - Docker image: installation/docker-image.md + - Usage: + - Minimal configuration: usage/minimal-configuration.md + - Application configuration: usage/application-configuration.md + - Process configuration: + - Miscellaneous: usage/process-configuration/miscellaneous.md + - Output prefix: usage/process-configuration/output-prefix.md + - Verbosity: usage/process-configuration/verbosity.md + - Bootstrap / tear down: usage/bootstrap-tear-down.md + - Output verbosity: usage/output-verbosity.md + - Theme: + - Configuration: theme/configuration.md + - Create a theme: theme/create-a-theme.md + - Examples: + - Available examples: examples/available-examples.md + - Contributing: + - Tips for devs: contributing/tips-for-devs.md +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences + - attr_list + - md_in_html + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg +extra: + package_version: "1.1.0" +plugins: + - search + - macros diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..c5f6e24 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +mkdocs +mkdocs-material +mkdocs-macros-plugin diff --git a/docs/theme/configuration.md b/docs/theme/configuration.md new file mode 100644 index 0000000..285cce4 --- /dev/null +++ b/docs/theme/configuration.md @@ -0,0 +1,68 @@ +--- +hide: + - toc +--- + +# Themes + +`parallel-processes` contains 2 themes: `Default` and `Summary`. + +## Default theme + +This theme output processes status at regular interval, configured by [application refresh interval](../usage/application-configuration.md). + +The ouput is rewritten at each refresh interval. + +This theme is the default one, and should be used most of the time. + +## Summary theme + +This theme output only one line at the start: `Starting X processes...`. + +Processes status are not showned / refreshed. + +At the end, all processes status and output are written, only one time. + +This theme should be used in environments who can't rewrite output, like CI : GitHub Actions logs for example. + +# Configure a theme + +## In PHP configuration + +You can configure the theme in your PHP configuration: + +```php +setTheme(new Defaultheme()) + # Configure the theme to Summary + ->setTheme(new SummaryTheme()) +``` + +## In CLI + +You can configure the theme in CLI. + +`Default` theme (default value): + +```bash +php parallel-process.php --theme=default +``` + +`Summary` theme: + +```bash +php parallel-process.php --theme=summary +``` + +!!! info "CLI override PHP" + + The theme configured in the CLI overrides the one configured in PHP. diff --git a/docs/theme/create-a-theme.md b/docs/theme/create-a-theme.md new file mode 100644 index 0000000..8e1fd8e --- /dev/null +++ b/docs/theme/create-a-theme.md @@ -0,0 +1,59 @@ +--- +hide: + - toc +--- + +# Create a theme + +To create a theme, you have to create a class who implements +[ThemeInterface](https://github.com/steevanb/php-parallel-processes/blob/master/src/Console/Application/Theme/ThemeInterface.php). + +## ThemeInterface::outputStart() + +This method will be called before starting the processes. + +You can output headers here for example. + +## ThemeInterface::outputProcessesState() + +This method will be called at each [application refresh interval](../usage/application-configuration.md), to output processes state. + +## ThemeInterface::outputSummary() + +This method will be called after all processes are terminated. + +For example, you can take verbosity into account here to write processes outputs, add a footer etc. + +## Example + +See [DefaultTheme](https://github.com/steevanb/php-parallel-processes/blob/master/src/Console/Application/Theme/DefaultTheme.php). + +# How to use it + +## Configure it in PHP + +Like Default and Summary themes, call `ParallelProcessesApplication::setTheme()` with your theme instance: + +```php +setTheme(new FooTheme()); +``` + +## Configure it in CLI + +Like Default and Summary theme, add `--theme` in your CLI command, the value should be the FQCN of your theme: + +```bash +php parallel-process.php --theme=App\\FooTheme +``` + +!!! info "Creation or the theme object" + + Your theme class should not have parameters in `__construct()` to be used in CLI. + + See [ParallelProcessesApplication::defineThemeFromInput()](https://github.com/steevanb/php-parallel-processes/blob/master/src/Console/Application/ParallelProcessesApplication.php). + + If you need a different behavior, do not hesitate to contribute! diff --git a/docs/usage/application-configuration.md b/docs/usage/application-configuration.md new file mode 100644 index 0000000..76b5065 --- /dev/null +++ b/docs/usage/application-configuration.md @@ -0,0 +1,57 @@ +--- +hide: + - toc +--- + +# Global timeout + +Default value: `null` (no timeout). + +Configured timeout is in seconds. + +```php +setTimeout(10) +``` + +# Refresh interval + +You can configure the refresh interval, to scan all processes status and start the next ones, +with `ParallelProcessesApplication::setRefreshInterval()`. + +Default value: `10000` (10ms). + +Configured refresh interval is in microseconds. + +```php +setRefreshInterval(50000) +``` + +# Maximum processes in parallel + +Default value: `null` (no maximum). + +```php +setMaximumParallelProcesses(3) +``` + +# Theme + +See [Configure a theme](../theme/configuration.md). diff --git a/docs/usage/bootstrap-tear-down.md b/docs/usage/bootstrap-tear-down.md new file mode 100644 index 0000000..7e2aea4 --- /dev/null +++ b/docs/usage/bootstrap-tear-down.md @@ -0,0 +1,76 @@ +--- +hide: + - toc +--- + +# Boostrap process + +You can execute a process before any other process with a bootstrap process. + +Bootstrap process can be added with `ParallelProcessesApplication::addProcess()`, +and should be an instance of [BootstrapProcessInterface](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/BootstrapProcessInterface.php). + +You can use [BootstrapProcess](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/BoostrapProcess.php), +who implements `BootstrapProcessInterface`, +instead of [Process](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/Process.php). + +You can add this process when you want, before calling `ParallelProcessesApplication::run()`: before normal process, or after, it does not matter. + +!!! info "Bootstrap process failure" + If a bootstrap process fail, normal processes will not be executed. + + In this case, tear down processes will be executed. + +```php hl_lines="10 12" +addProcess(new BootstrapProcess(['bootstrap', 'process', '#1'])) + ->addProcess(new Process(['normal', 'process'])) + ->addProcess(new BootstrapProcess(['bootstrap', 'process', '#2'])) + ->run(new ArgvInput($argv)); +``` + +!!! bug "Typo in BootstrapProcess class name before 1.1.0" + Before 1.1.0, `BootstrapProcess` class name contains a typo: it was `BoostrapProcess`. + + It has been fixed in [1.1.0](https://github.com/steevanb/php-parallel-processes/blob/readthedocs/changelog.md): + a new `BootstrapProcess` class has been created, and `BoostrapProcess` has been depreciated. + +# Tear down process + +You can execute a process after all other processes with a tear down process. + +Tear down process can be added with `ParallelProcessesApplication::addProcess()`, +and should be an instance of [TearDownProcessInterface](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/TearDownProcessInterface.php). + +You can use [TearDownProcess](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/TearDownProcess.php), +who implements `TearDownProcessInterface`, +instead of [Process](https://github.com/steevanb/php-parallel-processes/blob/master/src/Process/Process.php). + +You can add this process when you want, before calling `ParallelProcessesApplication::run()`: before normal process, or after, it does not matter. + +!!! info "Tear down process execution rule" + The tear down processes will always be executed, whether the previous processes succeeded or failed. + +```php hl_lines="10 12" +addProcess(new TearDownProcess(['tear', 'down', 'process', '#1'])) + ->addProcess(new Process(['normal', 'process'])) + ->addProcess(new TearDownProcess(['tear', 'down', 'process', '#2'])) + ->run(new ArgvInput($argv)); +``` diff --git a/docs/usage/minimal-configuration.md b/docs/usage/minimal-configuration.md new file mode 100644 index 0000000..645156b --- /dev/null +++ b/docs/usage/minimal-configuration.md @@ -0,0 +1,36 @@ +--- +hide: + - toc +--- + +# Minimal configuration + +Here is an example of the minimal configuration, to have 2 processes running in parallel: + +```php +addProcess(new Process(['first', 'process'])) + ->addProcess(new Process(['second', 'process'])) + ->run(new ArgvInput($argv)); +``` + +# Advanced usage + +See [application configuration](application-configuration.md). + +See [process configuration](process-configuration/miscellaneous.md). diff --git a/docs/usage/output-verbosity.md b/docs/usage/output-verbosity.md new file mode 100644 index 0000000..df12974 --- /dev/null +++ b/docs/usage/output-verbosity.md @@ -0,0 +1,42 @@ +--- +hide: + - toc +--- + +# Output verbosity + +By default, default theme will output only the state, the process name and the process error output. + +You can add information, depending on the verbosity level. + +| Output | Normal | Verbose | Very verbose | Debug | +|-------------------------|:-----------------------------------------:|:-----------------------------------------:|:-----------------------------------------:|:-----------------------------------------:| +| Process state | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | +| Process name | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | +| Execution time | | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | +| Process standard output | | | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | +| Process error output | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | :material-checkbox-marked:{ .icon-green } | + +# Configure verbosity level + +Verbosity level is configured on the CLI command with `-v`, `-vv` or `-vvv`. + +**Normal**: default level, do not add anything. +```bash +php parallel-process.php +``` + +**Verbose**: add `-v`. +```bash +php parallel-process.php -v +``` + +**Very verbose**: add `-vv`. +```bash +php parallel-process.php -vv +``` + +**Debug**: add `-vvv`. +```bash +php parallel-process.php -vvv +``` diff --git a/docs/usage/process-configuration/miscellaneous.md b/docs/usage/process-configuration/miscellaneous.md new file mode 100644 index 0000000..c62720d --- /dev/null +++ b/docs/usage/process-configuration/miscellaneous.md @@ -0,0 +1,46 @@ +--- +hide: + - toc +--- + +# Name + +Default: Last word of the command. + +Example with `new Process(['/foo', 'bar', 'baz'])`: name will be `baz`. + +```php +setName('foo'); +``` + +# Canceled as error + +You can configure if the cancelation of a process is considered as an error (`1`) or not (`0`) for the exit code. + +Default: `true`. + +```php +setCanceledAsError(true); +``` + +See [example](https://github.com/steevanb/php-parallel-processes/tree/master/examples/usage/process-configuration/canceled-as-error.php). + +!!! example "Try it!" + + With `setCanceledAsError(true)` (default value): + ```bash + PP_CANCELED_AS_ERROR=true parallel-processes-example.sh usage/process-configuration/canceled-as-error.php ; echo "Exit code: $?" + ``` + With `setCanceledAsError(false)`: + ```bash + PP_CANCELED_AS_ERROR=false parallel-processes-example.sh usage/process-configuration/canceled-as-error.php ; echo "Exit code: $?" + ``` diff --git a/docs/usage/process-configuration/output-prefix.md b/docs/usage/process-configuration/output-prefix.md new file mode 100644 index 0000000..038fe84 --- /dev/null +++ b/docs/usage/process-configuration/output-prefix.md @@ -0,0 +1,76 @@ +--- +hide: + - toc +--- + +# State line prefix + +You can add a prefix before the state line only. + +Default: `null`. + +```php +setOutputStatePrefix('foo'); +``` + +See [example](https://github.com/steevanb/php-parallel-processes/tree/master/examples/usage/process-configuration/output-state-prefix.php). + +!!! example "Try it!" + + ```bash + parallel-processes-example.sh usage/process-configuration/output-state-prefix.php + ``` + +# Summary lines prefix + +You can add a prefix before process output lines. + +Default: `null`. + +```php +setOutputSummaryPrefix('foo'); +``` + +See [example](https://github.com/steevanb/php-parallel-processes/tree/master/examples/usage/process-configuration/output-summary_prefix.php). + +!!! info "Indentation" + + Indentation will be added after the configured summary prefix, to show output lines are for this process. + +!!! example "Try it!" + + ```bash + parallel-processes-example.sh usage/process-configuration/output-summaryprefix.php + ``` + +# All lines + +You can add an output prefix before any line. + +It's a shortcut for calling `Process::setOutputStatePrefix()` then `Process::setOutputSummaryPrefix()` with the same prefix. + +Default: `null`. + +```php +setOutputPrefix('foo'); +``` + +See [example](https://github.com/steevanb/php-parallel-processes/tree/master/examples/usage/process-configuration/output-prefix.php). + +!!! example "Try it!" + + ```bash + parallel-processes-example.sh usage/process-configuration/output-prefix.php + ``` diff --git a/docs/usage/process-configuration/verbosity.md b/docs/usage/process-configuration/verbosity.md new file mode 100644 index 0000000..0497a19 --- /dev/null +++ b/docs/usage/process-configuration/verbosity.md @@ -0,0 +1,77 @@ +--- +hide: + - toc +--- + +# Output verbosity + +All processes outputs will be written at the end of all processes execution. + +Nothing will be written during execution. + +See [verbosity](../output-verbosity.md). + +# Standard output verbosity + +Default: `OutputInterface::VERBOSITY_VERY_VERBOSE` (`-vv`). + +```php +setStandardOutputVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); +``` + +# Error output verbosity + +Default: `OutputInterface::VERBOSITY_VERY_VERBOSE` (`-vv`). + +```php +setErrorOutputVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); +``` + +# Canceled output verbosity + +Default: `OutputInterface::VERBOSITY_VERY_VERBOSE` (`-vv`). + +```php +setCanceledOutputVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); +``` + +# Standard output verbosity when the process fails + +Default: `OutputInterface::VERBOSITY_NORMAL`. + +```php +setFailureStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL); +``` + +# Error output verbosity when the process fails + +Default: `OutputInterface::VERBOSITY_NORMAL`. + +```php +setFailureErrorOutputVerbosity(OutputInterface::VERBOSITY_NORMAL); +``` diff --git a/examples/demo.php b/examples/demo.php new file mode 100644 index 0000000..0aa566e --- /dev/null +++ b/examples/demo.php @@ -0,0 +1,42 @@ +setName('Check installation'); + +$process1 = (new Process(['sleep', '2'])) + ->setName('Start docker stack'); + +$process2 = (new Process(['sleep', '1'])) + ->setName('Install dependencies'); +$process2->getStartCondition()->getProcessesSuccessful()->add($process1); + +$process3 = (new Process(['sleep', '2'])) + ->setName('Create database'); +$process3->getStartCondition()->getProcessesSuccessful()->add($process1); + +$process4 = (new Process(['sleep', '1'])) + ->setName('Clear cache'); + +$tearDownProcess = (new TearDownProcess(['sleep', '1'])) + ->setName('Wait everything is started'); + +(new ParallelProcessesApplication()) + ->addProcess($bootstrapProcess) + ->addProcess($process1) + ->addProcess($process2) + ->addProcess($process3) + ->addProcess($process4) + ->addProcess($tearDownProcess) + ->run(new ArgvInput($argv)); diff --git a/examples/parallel-processes-example.sh b/examples/parallel-processes-example.sh new file mode 100755 index 0000000..6aa8f00 --- /dev/null +++ b/examples/parallel-processes-example.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +ENV_ARGS=() +while IFS='=' read -r name value; do + ENV_ARGS+=("-e" "$name=$value") +done < <(env | grep '^PP_') + +readonly SOURCE_CODE_PATH=/composer/vendor/steevanb/php-parallel-processes + +LOCAL_SOURCE=true +VOLUME_ARGS=() +if [[ "${LOCAL_SOURCE,,}" == "true" ]]; then + VOLUME_ARGS+=("-v" "$(pwd):${SOURCE_CODE_PATH}") +fi + +readonly EXAMPLE="${1}" +shift + +docker \ + run \ + --rm \ + -it \ + "${ENV_ARGS[@]}" \ + "${VOLUME_ARGS[@]}" \ + steevanb/php-parallel-processes:1.1.0-alpine \ + php "${SOURCE_CODE_PATH}"/examples/"${EXAMPLE}" "$@" diff --git a/examples/usage/process-configuration/canceled-as-error.php b/examples/usage/process-configuration/canceled-as-error.php new file mode 100644 index 0000000..ef0900f --- /dev/null +++ b/examples/usage/process-configuration/canceled-as-error.php @@ -0,0 +1,35 @@ +setName('This process will fail') + ->setFailureErrorOutputVerbosity(OutputInterface::VERBOSITY_VERBOSE) + ->setSpreadErrorToApplicationExitCode(false); + +$canceledProcess = (new Process(['cancel-me'])) + ->setName('This proces will be canceled') + ->setCanceledAsError( + ($_ENV['PP_CANCELED_AS_ERROR'] ?? 'true') === 'true' + ); + +$canceledProcess + ->getStartCondition() + ->getProcessesSuccessful() + ->add($errorProcess); + +(new ParallelProcessesApplication()) + ->addProcess($errorProcess) + ->addProcess($canceledProcess) + ->run(new ArgvInput($argv)); diff --git a/examples/usage/process-configuration/output-prefix.php b/examples/usage/process-configuration/output-prefix.php new file mode 100644 index 0000000..63be4db --- /dev/null +++ b/examples/usage/process-configuration/output-prefix.php @@ -0,0 +1,27 @@ +addProcess( + (new Process(['pwd'])) + ->setOutputPrefix('--Output prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->addProcess( + (new Process(['pwd'])) + ->setOutputPrefix('--Another output prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->run(new ArgvInput($argv)); diff --git a/examples/usage/process-configuration/output-state-prefix.php b/examples/usage/process-configuration/output-state-prefix.php new file mode 100644 index 0000000..e2ac78c --- /dev/null +++ b/examples/usage/process-configuration/output-state-prefix.php @@ -0,0 +1,27 @@ +addProcess( + (new Process(['pwd'])) + ->setOutputStatePrefix('--Output state prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->addProcess( + (new Process(['pwd'])) + ->setOutputStatePrefix('--Another output state prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->run(new ArgvInput($argv)); diff --git a/examples/usage/process-configuration/output-summary-prefix.php b/examples/usage/process-configuration/output-summary-prefix.php new file mode 100644 index 0000000..12a6406 --- /dev/null +++ b/examples/usage/process-configuration/output-summary-prefix.php @@ -0,0 +1,27 @@ +addProcess( + (new Process(['pwd'])) + ->setOutputSummaryPrefix('--Output summary prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->addProcess( + (new Process(['pwd'])) + ->setOutputSummaryPrefix('--Another output summary prefix--') + ->setStandardOutputVerbosity(OutputInterface::VERBOSITY_NORMAL) + ) + ->run(new ArgvInput($argv));