├── .github
├── CODEOWNERS
├── FUNDING.yml
├── boring-cyborg.yml
├── renovate.json
├── settings.yml
└── workflows
│ ├── ci.yml
│ └── craft-release.yaml
├── LICENSE
├── README.md
├── benchmark
└── memory.php
├── bin
└── child-process.source
├── build
├── .travis.php.ini
├── custom_php_ini.sh
├── handle_brew_pkg.sh
├── osx_install_composer.sh
└── prepare_osx_env.sh
├── composer-require-checker.json
├── composer.json
├── composer.lock
├── etc
└── qa
│ ├── composer-require-checker.json
│ ├── phpcs.xml
│ ├── phpstan.neon
│ ├── phpunit.xml
│ └── psalm.xml
├── infection.json.dist
├── src
├── ChildInterface.php
├── ChildProcess
│ ├── ArgvEncoder.php
│ ├── DoesNotImplementChildInterfaceException.php
│ ├── Factory.php
│ ├── Options.php
│ └── Process.php
├── CommunicationWithProcessUnexpectedEndException.php
├── Factory.php
├── Messages
│ ├── ActionableMessageInterface.php
│ ├── Error.php
│ ├── Factory.php
│ ├── Line.php
│ ├── LineDecoder.php
│ ├── LineEncoder.php
│ ├── LineInterface.php
│ ├── Message.php
│ ├── Payload.php
│ ├── Rpc.php
│ ├── RpcError.php
│ ├── RpcSuccess.php
│ └── SecureLine.php
├── Messenger.php
├── MessengerInterface.php
├── OutstandingCall.php
├── OutstandingCallInterface.php
├── OutstandingCalls.php
├── ProcessUnexpectedEndException.php
├── ReturnChild.php
└── StaticConfig.php
└── var
└── .gitkeep
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @WyriHaximus
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: WyriHaximus
2 |
--------------------------------------------------------------------------------
/.github/boring-cyborg.yml:
--------------------------------------------------------------------------------
1 | labelPRBasedOnFilePath:
2 | "Documentation 📚":
3 | - README.md
4 | - CONTRIBUTING.md
5 | "Dependencies 📦":
6 | - Dockerfile*
7 | - composer.*
8 | - package.json
9 | - package-lock.json
10 | - yarn.lock
11 | "Docker 🐳":
12 | - Dockerfile*
13 | - .docker/**/*
14 | "Image 🖼":
15 | - "**/*.gif"
16 | - "**/*.jpg"
17 | - "**/*.jpeg"
18 | - "**/*.png"
19 | - "**/*.webp"
20 | "CSS 👩🎨":
21 | - "**/*.css"
22 | "HTML 👷♀️":
23 | - "**/*.htm"
24 | - "**/*.html"
25 | "NEON 🦹♂️":
26 | - "**/*.neon"
27 | "MarkDown 📝":
28 | - "**/*.md"
29 | "YAML 🍄":
30 | - "**/*.yml"
31 | - "**/*.yaml"
32 | "JSON 👨💼":
33 | - "**/*.json"
34 | "Go 🐹":
35 | - "**/*.go"
36 | "JavaScript 🦏":
37 | - "**/*.js"
38 | - package.json
39 | - package-lock.json
40 | - yarn.lock
41 | "PHP 🐘":
42 | - "**/*.php"
43 | - composer.*
44 | "Configuration ⚙":
45 | - .github/*
46 | "CI 🚧":
47 | - .github/workflows/*
48 | - .scrutinizer.yml
49 | "Templates 🌲":
50 | - "**/*.twig"
51 | - "**/*.tpl"
52 | "Helm ☸":
53 | - .helm/**/*
54 | "Tests 🧪":
55 | - tests/**/*
56 | "Source 🔮":
57 | - src/**/*
58 |
59 | labelerFlags:
60 | labelOnPRUpdates: true
61 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "packageRules": [
4 | {
5 | "managers": ["composer"],
6 | "rangeStrategy": "in-range-only"
7 | },
8 | {
9 | "managers": ["composer"],
10 | "rangeStrategy": "bump"
11 | },
12 | {
13 | "managers": ["composer"],
14 | "matchPackageNames": ["php"],
15 | "enabled": false
16 | }
17 | ],
18 | "extends": [
19 | "config:base",
20 | ":widenPeerDependencies",
21 | ":rebaseStalePrs",
22 | ":prHourlyLimitNone",
23 | ":prConcurrentLimitNone",
24 | "group:phpstan"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | repository:
2 | private: false
3 | has_issues: true
4 | has_wiki: false
5 | has_downloads: true
6 | default_branch: master
7 | allow_squash_merge: false
8 | allow_merge_commit: true
9 | allow_rebase_merge: false
10 |
11 | # Labels: define labels for Issues and Pull Requests
12 | labels:
13 | - name: "Dependencies 📦"
14 | color: 0025ff
15 | description: "Pull requests that update a dependency file"
16 | - name: "Image 🖼"
17 | color: 00ffff
18 | - name: "HTML 👷♀️"
19 | color: ffffff
20 | - name: "CSS 👩🎨"
21 | color: b3b3b3
22 | - name: "JavaScript 🦏"
23 | color: ffff00
24 | - name: "Go 🐹"
25 | color: 00ADD8
26 | - name: "JSON 👨💼"
27 | color: 00ADD8
28 | - name: "NEON 🦹♂️"
29 | color: CE3262
30 | - name: "MarkDown 📝"
31 | color: 000000
32 | - name: "YAML 🍄"
33 | color: ff1aff
34 | - name: "Templates 🌲"
35 | color: 009933
36 | - name: "Helm ☸"
37 | color: 091C84
38 | - name: "Tests 🧪"
39 | color: ffe6e6
40 | - name: "Source 🔮"
41 | color: e6ffe6
42 | - name: "Configuration ⚙"
43 | color: b3b3cc
44 | - name: "PHP 🐘"
45 | color: 8892BF
46 | description: "Hypertext Pre Processor"
47 | - name: "Docker 🐳"
48 | color: 0db7ed
49 | description: "Pull requests that relate to Docker"
50 | - name: "CI 🚧"
51 | color: ffff00
52 | - name: "Feature 🏗"
53 | color: 66ff99
54 | - name: "Documentation 📚"
55 | color: 6666ff
56 | - name: "Security 🕵️♀️"
57 | color: ff0000
58 | - name: "Hacktoberfest 🎃"
59 | color: 152347
60 | - name: "Bug 🐞"
61 | color: d73a4a
62 | description: "Something isn't working"
63 | oldname: bug
64 | - name: "Duplicate ♊"
65 | color: cfd3d7
66 | description: "This issue or pull request already exists"
67 | oldname: duplicate
68 | - name: "Enhancement ✨"
69 | color: a2eeef
70 | description: "New feature or request"
71 | oldname: enhancement
72 | - name: "Good First Issue"
73 | color: 7057ff
74 | description: "Good for newcomers"
75 | oldname: "good first issue"
76 | - name: "Help Wanted"
77 | color: 008672
78 | description: "Extra attention is needed"
79 | oldname: "help wanted"
80 | - name: Invalid
81 | color: e4e669
82 | description: "This doesn't seem right"
83 | oldname: invalid
84 | - name: "Question ❓"
85 | color: d876e3
86 | description: "Further information is requested"
87 | oldname: question
88 | - name: "Will not be fixed 🛑"
89 | color: ffffff
90 | description: "This will not be worked on"
91 | oldname: wontfix
92 | - name: "Sponsor Request ❤️"
93 | color: fedbf0
94 | description: "Issue/PR opened by sponsor"
95 |
96 | branches:
97 | - name: master
98 | protection:
99 | required_pull_request_reviews:
100 | required_approving_review_count: 1
101 | dismiss_stale_reviews: true
102 | require_code_owner_reviews: true
103 | # Required. Require status checks to pass before merging. Set to null to disable
104 | required_status_checks:
105 | # Required. Require branches to be up to date before merging.
106 | strict: true
107 | # Required. The list of status checks to require in order to merge into this branch
108 | contexts: []
109 | # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.
110 | enforce_admins: true
111 | # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable.
112 | restrictions:
113 | apps: []
114 | users: []
115 | teams: []
116 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 | on:
3 | push:
4 | branches:
5 | - 'main'
6 | - 'master'
7 | - 'refs/heads/v[0-9]+.[0-9]+.[0-9]+'
8 | pull_request:
9 | ## This workflow needs the `pull-request` permissions to work for the package diffing
10 | ## Refs: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions
11 | permissions:
12 | pull-requests: write
13 | contents: read
14 | jobs:
15 | ci:
16 | name: Continuous Integration
17 | uses: WyriHaximus/github-workflows/.github/workflows/package.yaml@main
18 |
--------------------------------------------------------------------------------
/.github/workflows/craft-release.yaml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 | env:
3 | MILESTONE: ${{ github.event.milestone.title }}
4 | on:
5 | milestone:
6 | types:
7 | - closed
8 | jobs:
9 | wait-for-status-checks:
10 | name: Wait for status checks
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - run: sleep 13
15 | - name: 'Wait for status checks'
16 | id: waitforstatuschecks
17 | uses: "WyriHaximus/github-action-wait-for-status@master"
18 | with:
19 | ignoreActions: "Wait for status checks"
20 | checkInterval: 5
21 | env:
22 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
23 | - id: generate-version-strategy
24 | if: steps.waitforstatuschecks.outputs.status != 'success'
25 | name: Fail
26 | run: exit 1
27 | generate-changelog:
28 | name: Generate Changelog
29 | needs:
30 | - wait-for-status-checks
31 | runs-on: ubuntu-latest
32 | outputs:
33 | changelog: ${{ steps.changelog.outputs.changelog }}
34 | steps:
35 | - name: Generate changelog
36 | uses: WyriHaximus/github-action-jwage-changelog-generator@v1
37 | id: changelog
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | with:
41 | milestone: ${{ env.MILESTONE }}
42 | - name: Show changelog
43 | run: echo "${CHANGELOG}"
44 | env:
45 | CHANGELOG: ${{ steps.changelog.outputs.changelog }}
46 | create-release:
47 | name: Create Release
48 | needs:
49 | - generate-changelog
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v4
53 | env:
54 | CHANGELOG: ${{ needs.generate-changelog.outputs.changelog }}
55 | - run: |
56 | echo -e "${MILESTONE_DESCRIPTION}\r\n\r\n${CHANGELOG}" > release-${{ env.MILESTONE }}-release-message.md
57 | cat release-${{ env.MILESTONE }}-release-message.md
58 | release_message=$(cat release-${{ env.MILESTONE }}-release-message.md)
59 | release_message="${release_message//'%'/'%25'}"
60 | release_message="${release_message//$'\n'/'%0A'}"
61 | release_message="${release_message//$'\r'/'%0D'}"
62 | echo "::set-output name=release_message::$release_message"
63 | id: releasemessage
64 | env:
65 | MILESTONE_DESCRIPTION: ${{ github.event.milestone.description }}
66 | CHANGELOG: ${{ needs.generate-changelog.outputs.changelog }}
67 | - name: Create Reference Release with Changelog
68 | uses: fleskesvor/create-release@feature/support-target-commitish
69 | env:
70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71 | with:
72 | tag_name: ${{ env.MILESTONE }}
73 | release_name: ${{ env.MILESTONE }}
74 | body: ${{ steps.releasemessage.outputs.release_message }}
75 | draft: false
76 | prerelease: false
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Cees-Jan Kiewiet
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReactPHP Child Process Messenger
2 |
3 | [](https://github.com/WyriHaximus/reactphp-child-process-messenger/actions/workflows/ci.yml)
4 | [](https://packagist.org/packages/WyriHaximus/react-child-process-messenger)
5 | [](https://packagist.org/packages/WyriHaximus/react-child-process-messenger)
6 | [](https://scrutinizer-ci.com/g/WyriHaximus/reactphp-child-process-messenger/?branch=master)
7 | [](https://packagist.org/packages/wyrihaximus/react-child-process-messenger)
8 |
9 | Plain messages and RPC style wrapper around [`react/child-process`](https://github.com/reactphp/child-process). For pooling messengers take a look at [`wyrihaximus/react-child-process-pool`](https://github.com/WyriHaximus/reactphp-child-process-pool)
10 |
11 | ### Installation ###
12 |
13 | To install via [Composer](http://getcomposer.org/), use the command below, it will automatically detect the latest version and bind it with `~`.
14 |
15 | ```
16 | composer require wyrihaximus/react-child-process-messenger
17 | ```
18 |
19 | ## Hassle less Example ##
20 |
21 | While this package supports several ways of setting up communication between parent and child the simplest way is to create class implementing `WyriHaximus\React\ChildProcess\Messenger\ChildInterface`. Up on calling `create` everything is set up and created to handle supported `RPC`'s and messages.
22 |
23 | ```php
24 | registerRpc('example', function (Payload $payload) {
37 | return resolve($payload->getPayload());
38 | });
39 | }
40 | }
41 | ```
42 |
43 | On the parent side you only need to call to spawn a child running that class:
44 |
45 | ```php
46 | MessengerFactory::parentFromClass('ExampleChild', $loop)->then(static function (Messenger $messenger): void {
47 | $messenger->rpc(/* etc etc */);
48 | });
49 | ```
50 |
51 | ## More Examples ##
52 |
53 | For both message and RPC examples see the [examples](https://github.com/WyriHaximus/reactphp-child-process-messenger/tree/master/examples) directory
54 |
55 | ## Contributing ##
56 |
57 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
58 |
59 | ## License ##
60 |
61 | Copyright 2021 [Cees-Jan Kiewiet](http://wyrihaximus.net/)
62 |
63 | Permission is hereby granted, free of charge, to any person
64 | obtaining a copy of this software and associated documentation
65 | files (the "Software"), to deal in the Software without
66 | restriction, including without limitation the rights to use,
67 | copy, modify, merge, publish, distribute, sublicense, and/or sell
68 | copies of the Software, and to permit persons to whom the
69 | Software is furnished to do so, subject to the following
70 | conditions:
71 |
72 | The above copyright notice and this permission notice shall be
73 | included in all copies or substantial portions of the Software.
74 |
75 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
76 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
77 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
78 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
79 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
80 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
81 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
82 | OTHER DEALINGS IN THE SOFTWARE.
83 |
--------------------------------------------------------------------------------
/benchmark/memory.php:
--------------------------------------------------------------------------------
1 | then(function (Messenger $messenger) use ($loop) {
32 | $messenger->on('error', function ($e) {
33 | echo 'Error: ', \var_export($e, true), PHP_EOL;
34 | });
35 |
36 | $i = 0;
37 | $loop->addPeriodicTimer(0.00001, function (TimerInterface $timer) use (&$i, $messenger, $loop) {
38 | if ($i >= I) {
39 | $loop->cancelTimer($timer);
40 | $messenger->softTerminate();
41 |
42 | show_memory('Completed messaging');
43 |
44 | $loop->addTimer(5, function () {
45 | });
46 |
47 | return;
48 | }
49 |
50 | $messenger->rpc(MessagesFactory::rpc('return', [
51 | 'i' => $i,
52 | 'time' => \time(),
53 | ]));
54 |
55 | $i++;
56 | });
57 | })->done();
58 |
59 | $loop->run();
60 |
61 | show_memory('Done');
62 |
63 | unset($loop);
64 | $loop = null;
65 |
66 | show_memory('Removed loop');
67 |
68 | $cycles = \gc_collect_cycles();
69 |
70 | show_memory('gc_collect_cycles: ' . $cycles);
71 |
--------------------------------------------------------------------------------
/bin/child-process.source:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 | "${PHP_INI_SCANDIR}/travis.ini"
18 | echo "Added php.ini from ${ADDITIONAL_PHP_INI} to ${PHP_INI_SCANDIR}/travis.ini"
19 |
20 | elif [[ "${_PHP}" == hhv* ]]; then
21 | echo "--Copy file for HHVM @ OSX--"
22 | fi
23 |
24 | elif [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then
25 | if [[ "${TRAVIS_PHP_VERSION}" == php* ]]; then
26 | phpenv config-add "${ADDITIONAL_PHP_INI}"
27 | echo "Added php.ini from ${ADDITIONAL_PHP_INI} to phpenv"
28 |
29 | elif [[ "${TRAVIS_PHP_VERSION}" == hhv* ]]; then
30 | cat build/.travis.php.ini >> /etc/hhvm/php.ini
31 | echo "Added php.ini content from ${ADDITIONAL_PHP_INI} to /etc/hhvm/php.ini"
32 | fi
33 | fi
34 |
--------------------------------------------------------------------------------
/build/handle_brew_pkg.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ "$#" -eq 1 ]]; then
4 | echo "Handling \"$1\" brew package..."
5 | else
6 | echo "Brew failed - invalid $0 call"
7 | exit 1;
8 | fi
9 |
10 | if [[ $(brew ls --versions "$1") ]]; then
11 | if brew outdated "$1"; then
12 | echo "Package upgrade is not required, skipping"
13 | else
14 | echo "Updating package...";
15 | brew upgrade "$1"
16 | if [ $? -ne 0 ]; then
17 | echo "Upgrade failed"
18 | exit 1
19 | fi
20 | fi
21 | else
22 | echo "Package not available - installing..."
23 | brew install "$1"
24 | if [ $? -ne 0 ]; then
25 | echo "Install failed"
26 | exit 1
27 | fi
28 | fi
29 |
30 | echo "Linking installed package..."
31 | brew link "$1"
32 |
--------------------------------------------------------------------------------
/build/osx_install_composer.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
3 | php -r "if (hash_file('SHA384', 'composer-setup.php') === '070854512ef404f16bac87071a6db9fd9721da1684cd4589b1196c3faf71b9a2682e2311b36a5079825e155ac7ce150d') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
4 | sudo php composer-setup.php --install-dir=/usr/local/bin/ --filename=composer
5 | php -r "unlink('composer-setup.php');"
6 |
--------------------------------------------------------------------------------
/build/prepare_osx_env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "Here's the OSX environment:"
4 | sw_vers
5 | brew --version
6 |
7 | echo "Updating brew..."
8 | brew update
9 |
10 | if [[ "${_PHP}" == "hhvm" ]]; then
11 | echo "Adding brew HHVM dependencies..."
12 | brew tap hhvm/hhvm
13 |
14 | else
15 | echo "Adding brew PHP dependencies..."
16 | brew tap homebrew/dupes
17 | brew tap homebrew/versions
18 | brew tap homebrew/homebrew-php
19 | fi
20 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist" : [
3 | "null", "true", "false",
4 | "static", "self", "parent",
5 | "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object",
6 | "validate_array", "throwable_decode", "throwable_encode", "throwable_json_decode", "throwable_json_encode"
7 | ],
8 | "php-core-extensions" : [
9 | "Core",
10 | "date",
11 | "pcre",
12 | "Phar",
13 | "Reflection",
14 | "SPL",
15 | "standard"
16 | ],
17 | "scan-files" : []
18 | }
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wyrihaximus/react-child-process-messenger",
3 | "description": "Messenger decorator for react/child-process",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Cees-Jan Kiewiet",
8 | "email": "ceesjank@gmail.com",
9 | "homepage": "http://wyrihaximus.net/"
10 | }
11 | ],
12 | "require": {
13 | "php": "^8 || ^7.4",
14 | "ext-hash": "^8 || ^7.4",
15 | "ext-json": "^8 || ^7.4",
16 | "cakephp/utility": "^4.2.4",
17 | "doctrine/inflector": "^2.0.3",
18 | "evenement/evenement": "^3.0.1",
19 | "paragonie/random_compat": "^9.0 || ^2.0",
20 | "react/child-process": "^0.6.2",
21 | "react/event-loop": "^1.2",
22 | "react/promise": "^2.8",
23 | "react/promise-stream": "^1.2",
24 | "react/promise-timer": "^1.6",
25 | "react/socket": "^1.9",
26 | "symfony/polyfill-php81": "^1.23",
27 | "thecodingmachine/safe": "^1.3.3 || ^2.0 || ^3.0",
28 | "wyrihaximus/composer-update-bin-autoload-path": "^1.1.1",
29 | "wyrihaximus/constants": "^1.6",
30 | "wyrihaximus/file-descriptors": "^1.1",
31 | "wyrihaximus/json-throwable": "^4.2.0",
32 | "wyrihaximus/ticking-promise": "^3"
33 | },
34 | "require-dev": {
35 | "phpstan/phpstan-phpunit": "^0.12.22",
36 | "wyrihaximus/async-test-utilities": "^4.2.2"
37 | },
38 | "autoload": {
39 | "psr-4": {
40 | "WyriHaximus\\React\\ChildProcess\\Messenger\\": "src/"
41 | }
42 | },
43 | "autoload-dev": {
44 | "psr-4": {
45 | "WyriHaximus\\React\\Tests\\ChildProcess\\Messenger\\": "tests/"
46 | },
47 | "files": [
48 | "examples/ExamplesChildProcess.php"
49 | ]
50 | },
51 | "config": {
52 | "allow-plugins": {
53 | "dealerdirect/phpcodesniffer-composer-installer": true,
54 | "infection/extension-installer": true,
55 | "ergebnis/composer-normalize": true,
56 | "icanhazstring/composer-unused": true,
57 | "wyrihaximus/composer-update-bin-autoload-path": true
58 | },
59 | "platform": {
60 | "php": "7.4.7"
61 | },
62 | "sort-packages": true
63 | },
64 | "extra": {
65 | "unused": [
66 | "php",
67 | "react/promise-stream",
68 | "symfony/polyfill-php81"
69 | ],
70 | "wyrihaximus": {
71 | "bin-autoload-path-update": [
72 | "bin/child-process"
73 | ]
74 | }
75 | },
76 | "scripts": {
77 | "post-install-cmd": [
78 | "composer normalize"
79 | ],
80 | "post-update-cmd": [
81 | "composer normalize"
82 | ]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/etc/qa/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist" : [
3 | "null", "true", "false",
4 | "static", "self", "parent",
5 | "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object",
6 | "Safe\\date", "WyriHaximus\\Constants\\Boolean\\FALSE_", "WyriHaximus\\Constants\\Boolean\\TRUE_",
7 | "WyriHaximus\\Constants\\Numeric\\ONE_FLOAT"
8 | ],
9 | "php-core-extensions" : [
10 | "Core",
11 | "date",
12 | "pcre",
13 | "Phar",
14 | "Reflection",
15 | "SPL",
16 | "standard"
17 | ],
18 | "scan-files" : []
19 | }
20 |
--------------------------------------------------------------------------------
/etc/qa/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ../../src
10 | ../../tests
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/etc/qa/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | excludes_analyse:
3 | - tests/*Exception.php
4 | - tests/MissingAttributes.php
5 | ergebnis:
6 | classesAllowedToBeExtended:
7 | - Exception
8 | - WyriHaximus\TestUtilities\TestCase
9 |
10 | includes:
11 | - ../../vendor/wyrihaximus/test-utilities/rules.neon
12 |
--------------------------------------------------------------------------------
/etc/qa/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ../../tests/
6 |
7 |
8 |
9 |
10 | ../../src/
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/etc/qa/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 120,
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "logs": {
9 | "text": "./var/infection.log",
10 | "summary": "./var/infection-summary.log",
11 | "json": "./var/infection.json",
12 | "perMutator": "./var/infection-per-mutator.md"
13 | },
14 | "mutators": {
15 | "@default": false
16 | },
17 | "phpUnit": {
18 | "configDir": "./etc/qa/"
19 | }
20 | }
--------------------------------------------------------------------------------
/src/ChildInterface.php:
--------------------------------------------------------------------------------
1 | toArray()));
17 | }
18 |
19 | public static function decode(string $argv): Options
20 | {
21 | return new Options(...json_decode(base64_decode($argv, true)));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/ChildProcess/DoesNotImplementChildInterfaceException.php:
--------------------------------------------------------------------------------
1 | class = $class;
20 |
21 | return $self;
22 | }
23 |
24 | public function class(): string
25 | {
26 | return $this->class;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/ChildProcess/Factory.php:
--------------------------------------------------------------------------------
1 | then(static function (Messenger $messenger) use ($loop): void {
19 | Process::create($loop, $messenger);
20 | })->then(null, static function () use ($loop, &$exitCode): void {
21 | $loop->stop();
22 | $exitCode = 1;
23 | });
24 |
25 | $loop->run();
26 |
27 | return $exitCode;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/ChildProcess/Options.php:
--------------------------------------------------------------------------------
1 | random = $random;
16 | $this->address = $address;
17 | $this->connectTimeout = $connectTimeout;
18 | }
19 |
20 | public function random(): string
21 | {
22 | return $this->random;
23 | }
24 |
25 | public function address(): string
26 | {
27 | return $this->address;
28 | }
29 |
30 | public function connectTimeout(): int
31 | {
32 | return $this->connectTimeout;
33 | }
34 |
35 | /**
36 | * @return array
37 | */
38 | public function toArray(): array
39 | {
40 | return [
41 | $this->random,
42 | $this->address,
43 | $this->connectTimeout,
44 | ];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/ChildProcess/Process.php:
--------------------------------------------------------------------------------
1 | loop = $loop;
27 | $this->messenger = $messenger;
28 | $this->messenger->registerRpc(
29 | MessengerFactory::PROCESS_REGISTER,
30 | function (Payload $payload): PromiseInterface {
31 | /**
32 | * @psalm-suppress PossiblyNullArgument
33 | */
34 | if (! is_subclass_of($payload['className'], ChildInterface::class)) {
35 | throw DoesNotImplementChildInterfaceException::create($payload['className'] ?? '');
36 | }
37 |
38 | ($payload['className'])::create($this->messenger, $this->loop);
39 | $this->deregisterRpc();
40 |
41 | return resolve([]);
42 | }
43 | );
44 | }
45 |
46 | public static function create(LoopInterface $loop, Messenger $messenger): void
47 | {
48 | $reject = static function (Throwable $exeption) use ($loop): void {
49 | // $messenger->error(MessagesFactory::error($exeption->getFile()));
50 | $loop->addTimer(1, static function () use ($loop): void {
51 | $loop->stop();
52 | });
53 | };
54 |
55 | try {
56 | new Process($loop, $messenger);
57 | } catch (Throwable $throwable) { /** @phpstan-ignore-line */
58 | $reject($throwable);
59 | }
60 | }
61 |
62 | private function deregisterRpc(): void
63 | {
64 | $this->loop->futureTick(function (): void {
65 | $this->messenger->deregisterRpc(MessengerFactory::PROCESS_REGISTER);
66 | });
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/CommunicationWithProcessUnexpectedEndException.php:
--------------------------------------------------------------------------------
1 | $options
50 | */
51 | public static function parent(
52 | Process $process,
53 | LoopInterface $loop,
54 | array $options = []
55 | ): Promise\PromiseInterface {
56 | return new Promise\Promise(static function (callable $resolve, callable $reject) use ($process, $loop, $options): void {
57 | $server = new SocketServer('127.0.0.1:0', [], $loop);
58 |
59 | $options['random'] = bin2hex(random_bytes(32));
60 | $options['address'] = (string) $server->getAddress();
61 | $options = new Options($options['random'], $options['address'], $options['connect-timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT);
62 | $argvString = escapeshellarg(ArgvEncoder::encode($options));
63 | $process = new Process($process->getCommand() . ' ' . $argvString);
64 |
65 | self::startParent($process, $server, $loop, $options)->then($resolve, $reject);
66 | });
67 | }
68 |
69 | /**
70 | * @param array $options
71 | *
72 | * @return Promise\PromiseInterface
73 | *
74 | * @psalm-suppress TooManyTemplateParams
75 | * @phpstan-ignore-next-line
76 | */
77 | public static function parentFromClass(
78 | string $class,
79 | LoopInterface $loop,
80 | array $options = []
81 | ): Promise\PromiseInterface {
82 | if (! is_subclass_of($class, ChildInterface::class)) {
83 | /** @phpstan-ignore-next-line */
84 | throw new Exception('Given class doesn\'t implement ChildInterface');
85 | }
86 |
87 | return new Promise\Promise(static function (callable $resolve, callable $reject) use ($class, $loop, $options): void {
88 | $template = '%s';
89 | if (array_key_exists('cmdTemplate', $options)) {
90 | $template = $options['cmdTemplate'];
91 | unset($options['cmdTemplate']);
92 | }
93 |
94 | $fds = [];
95 | if (StaticConfig::shouldListFileDescriptors() && DIRECTORY_SEPARATOR !== '\\') {
96 | if (array_key_exists('fileDescriptorLister', $options) && $options['fileDescriptorLister'] instanceof ListerInterface) {
97 | $fileDescriptorLister = $options['fileDescriptorLister'];
98 | unset($options['fileDescriptorLister']);
99 | } else {
100 | $fileDescriptorLister = FileDescriptorsFactory::create();
101 | }
102 |
103 | foreach ($fileDescriptorLister->list() as $id) {
104 | $fds[(int) $id] = ['file', '/dev/null', 'r'];
105 | }
106 | }
107 |
108 | $server = new SocketServer('127.0.0.1:0', [], $loop);
109 | $connectTimeout = $options['connect-timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT;
110 | $options = new Options(bin2hex(random_bytes(32)), (string) $server->getAddress(), $options['connect-timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT);
111 |
112 | $phpBinary = escapeshellarg(PHP_BINARY . (PHP_SAPI === 'phpdbg' ? ' -qrr --' : ''));
113 | $childProcessPath = escapeshellarg(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'child-process');
114 | $argvString = escapeshellarg(ArgvEncoder::encode($options));
115 | $command = $phpBinary . ' ' . $childProcessPath;
116 |
117 | $process = new Process(
118 | sprintf(
119 | $template,
120 | $command . ' ' . $argvString
121 | ),
122 | null,
123 | null,
124 | $fds
125 | );
126 |
127 | futurePromise()->then(static function () use ($process, $server, $loop, $options, $connectTimeout): Promise\PromiseInterface {
128 | return Promise\Timer\timeout(self::startParent($process, $server, $loop, $options), $connectTimeout, $loop);
129 | })->then(static function (Messenger $messenger) use ($class): Promise\PromiseInterface {
130 | return $messenger->rpc(MessengesFactory::rpc(Factory::PROCESS_REGISTER, ['className' => $class]))->then(static function () use ($messenger): Promise\PromiseInterface {
131 | return Promise\resolve($messenger);
132 | });
133 | })->then($resolve, $reject);
134 | });
135 | }
136 |
137 | /**
138 | * @phpstan-ignore-next-line
139 | */
140 | public static function child(LoopInterface $loop, Options $options, ?callable $termiteCallable = null): Promise\PromiseInterface
141 | {
142 | $connectTimeout = $options->connectTimeout();
143 |
144 | return (new Connector($loop, ['timeout' => $connectTimeout]))->connect($options->address())->then(static function (ConnectionInterface $connection) use ($options, $loop, $connectTimeout): Promise\PromiseInterface {
145 | return new Promise\Promise(static function (callable $resolve, callable $reject) use ($connection, $options, $loop, $connectTimeout): void {
146 | Promise\Timer\timeout(Promise\Stream\first($connection), $connectTimeout, $loop)->then(static function (string $chunk) use ($resolve, $reject, $connection, $options, $loop): void {
147 | [$confirmation] = explode(PHP_EOL, $chunk);
148 | if ($confirmation === 'syn') {
149 | $connection->write('ack' . PHP_EOL);
150 | $resolve(new Messenger($connection, $options));
151 | $connection->on('close', [$loop, 'stop']);
152 | $connection->on('error', [$loop, 'stop']);
153 |
154 | return;
155 | }
156 |
157 | $reject(new RuntimeException('Handshake SYN failed'));
158 | }, $reject);
159 | $connection->write(hash_hmac('sha512', $options->address(), $options->random()) . PHP_EOL);
160 | });
161 | })->then(static function (Messenger $messenger) use ($loop, $termiteCallable): Messenger {
162 | if ($termiteCallable === null) {
163 | $termiteCallable = static function () use ($loop): void {
164 | $loop->addTimer(
165 | self::TERMINATE_TIMEOUT,
166 | [
167 | $loop,
168 | 'stop',
169 | ]
170 | );
171 | };
172 | }
173 |
174 | $messenger->registerRpc(
175 | Messenger::TERMINATE_RPC,
176 | static function (Payload $payload, Messenger $messenger) use ($termiteCallable): Promise\PromiseInterface {
177 | $messenger->emit('terminate', [$messenger]);
178 | $termiteCallable();
179 |
180 | return Promise\resolve([]);
181 | }
182 | );
183 |
184 | return $messenger;
185 | });
186 | }
187 |
188 | private static function startParent(
189 | Process $process,
190 | SocketServer $server,
191 | LoopInterface $loop,
192 | Options $options
193 | ): Promise\PromiseInterface {
194 | return (new Promise\Promise(static function (callable $resolve, callable $reject) use ($process, $server, $loop, $options): void {
195 | $server->on(
196 | 'connection',
197 | static function (ConnectionInterface $connection) use ($server, $resolve, $reject, $options): void {
198 | Promise\Stream\first($connection)->then(static function (string $chunk) use ($options, $connection): Promise\PromiseInterface {
199 | [$confirmation] = explode(PHP_EOL, $chunk);
200 | if ($confirmation === hash_hmac('sha512', $options->address(), $options->random())) {
201 | $connection->write('syn' . PHP_EOL);
202 |
203 | return Promise\Stream\first($connection);
204 | }
205 |
206 | return Promise\reject(new RuntimeException('Signature mismatch'));
207 | })->then(static function (string $chunk) use ($options, $connection): Promise\PromiseInterface {
208 | [$confirmation] = explode(PHP_EOL, $chunk);
209 | if ($confirmation === 'ack') {
210 | return Promise\resolve(new Messenger($connection, $options));
211 | }
212 |
213 | return Promise\reject(new RuntimeException('Handshake failed'));
214 | })->then(static function (MessengerInterface $messenger) use ($server, $resolve): void {
215 | $server->close();
216 | $server->removeAllListeners('connection');
217 | $resolve($messenger);
218 | }, static function (Throwable $throwable) use ($server, $reject): void {
219 | $server->close();
220 | $server->removeAllListeners();
221 | $reject($throwable);
222 | });
223 | }
224 | );
225 | $server->on('error', static function (Throwable $et) use ($reject): void {
226 | $reject($et);
227 | });
228 |
229 | $process->start($loop);
230 | }, static function () use ($server, $process): void {
231 | $server->close();
232 | $server->removeAllListeners();
233 | $process->terminate();
234 | }))->then(static function (Messenger $messenger) use ($loop, $process): Messenger {
235 | $loop->addPeriodicTimer(self::INTERVAL, static function (TimerInterface $timer) use ($messenger, $loop, $process): void {
236 | if ($process->isRunning()) {
237 | return;
238 | }
239 |
240 | $loop->cancelTimer($timer);
241 |
242 | $exitCode = $process->getExitCode();
243 | if ($exitCode === 0) {
244 | return;
245 | }
246 |
247 | if ($exitCode === null) {
248 | return;
249 | }
250 |
251 | $messenger->crashed($exitCode);
252 | });
253 |
254 | return $messenger;
255 | });
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/Messages/ActionableMessageInterface.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
19 | }
20 |
21 | public function getPayload(): Throwable
22 | {
23 | return $this->payload;
24 | }
25 |
26 | /**
27 | * @return array>
28 | */
29 | public function jsonSerialize(): array
30 | {
31 | return [
32 | 'type' => 'error',
33 | 'payload' => throwable_encode($this->payload),
34 | ];
35 | }
36 |
37 | public function handle(object $bindTo, string $source): void
38 | {
39 | $cb = function (Throwable $payload): void {
40 | /**
41 | * @psalm-suppress UndefinedMethod
42 | */
43 | $this->emit('error', [ /** @phpstan-ignore-line */
44 | $payload,
45 | $this,
46 | ]);
47 | };
48 | $cb = $cb->bindTo($bindTo);
49 | /**
50 | * @psalm-suppress PossiblyInvalidFunctionCall
51 | */
52 | $cb($this->payload);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Messages/Factory.php:
--------------------------------------------------------------------------------
1 | $lineOptions
20 | */
21 | public static function fromLine(string $line, array $lineOptions): ActionableMessageInterface
22 | {
23 | $line = json_decode($line, true);
24 | self::$inflector ??= InflectorFactory::create()->build();
25 | $method = self::$inflector->camelize($line['type']);
26 | if ($method === 'secure') {
27 | return static::secureFromLine($line, $lineOptions);
28 | }
29 |
30 | if ($method === 'message') {
31 | return static::messageFromLine($line);
32 | }
33 |
34 | if ($method === 'error') {
35 | return static::errorFromLine($line);
36 | }
37 |
38 | if ($method === 'rpc') {
39 | return static::rpcFromLine($line);
40 | }
41 |
42 | if ($method === 'rpcError') {
43 | return static::rpcErrorFromLine($line);
44 | }
45 |
46 | if ($method === 'rpcSuccess') {
47 | return static::rpcSuccessFromLine($line);
48 | }
49 |
50 | /** @phpstan-ignore-next-line */
51 | throw new Exception('Unknown message type: ' . $line['type']);
52 | }
53 |
54 | /**
55 | * @param array $payload
56 | */
57 | public static function message(array $payload = []): Message
58 | {
59 | return new Message(new Payload($payload));
60 | }
61 |
62 | public static function error(Throwable $payload): Error
63 | {
64 | return new Error($payload);
65 | }
66 |
67 | /**
68 | * @param array $payload
69 | * @param mixed $uniqid
70 | */
71 | public static function rpc(string $target, array $payload = [], $uniqid = ''): Rpc
72 | {
73 | return new Rpc($target, new Payload($payload), $uniqid);
74 | }
75 |
76 | public static function rpcError(string $uniqid, Throwable $et): RpcError
77 | {
78 | return new RpcError($uniqid, $et);
79 | }
80 |
81 | /**
82 | * @param array $payload
83 | */
84 | public static function rpcSuccess(string $uniqid, array $payload = []): RpcSuccess
85 | {
86 | return new RpcSuccess($uniqid, new Payload($payload));
87 | }
88 |
89 | /**
90 | * @param array $line
91 | * @param array $lineOptions
92 | */
93 | private static function secureFromLine(array $line, array $lineOptions): ActionableMessageInterface
94 | {
95 | return SecureLine::fromLine($line, $lineOptions);
96 | }
97 |
98 | /**
99 | * @param array $line
100 | */
101 | private static function messageFromLine(array $line): Message
102 | {
103 | return static::message($line['payload']);
104 | }
105 |
106 | /**
107 | * @param array $line
108 | */
109 | private static function errorFromLine(array $line): Error
110 | {
111 | return static::error(LineDecoder::decode($line['payload'])['throwable']);
112 | }
113 |
114 | /**
115 | * @param array $line
116 | */
117 | private static function rpcFromLine(array $line): Rpc
118 | {
119 | return static::rpc($line['target'], $line['payload'], $line['uniqid']);
120 | }
121 |
122 | /**
123 | * @param array $line
124 | */
125 | private static function rpcErrorFromLine(array $line): RpcError
126 | {
127 | return static::rpcError($line['uniqid'], LineDecoder::decode($line['payload'])['throwable']);
128 | }
129 |
130 | /**
131 | * @param array $line
132 | */
133 | private static function rpcSuccessFromLine(array $line): RpcSuccess
134 | {
135 | return static::rpcSuccess($line['uniqid'], $line['payload']);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/Messages/Line.php:
--------------------------------------------------------------------------------
1 | $options
17 | *
18 | * @phpstan-ignore-next-line
19 | */
20 | public function __construct(ActionableMessageInterface $payload, array $options)
21 | {
22 | $this->payload = $payload;
23 | }
24 |
25 | public function __toString(): string
26 | {
27 | return json_encode($this->payload) . LineInterface::EOL;
28 | }
29 |
30 | public function getPayload(): JsonSerializable
31 | {
32 | return $this->payload;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Messages/LineDecoder.php:
--------------------------------------------------------------------------------
1 | $line
17 | *
18 | * @return array
19 | *
20 | * @throws ExceptionInterface
21 | * @throws ReflectionException
22 | */
23 | public static function decode(array $line): array
24 | {
25 | if ($line[LineEncoder::META_KEY][LineEncoder::TYPE_KEY] === LineEncoder::TYPE_THROWABLE) {
26 | return ['throwable' => throwable_decode($line[LineEncoder::VALUE_KEY])];
27 | }
28 |
29 | foreach ($line[LineEncoder::META_KEY][LineEncoder::THROWABLES_KEY] as $throwableKey) {
30 | $line[LineEncoder::VALUE_KEY][$throwableKey] = throwable_decode($line[LineEncoder::VALUE_KEY][$throwableKey]);
31 | }
32 |
33 | return Hash::expand($line[LineEncoder::VALUE_KEY]);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Messages/LineEncoder.php:
--------------------------------------------------------------------------------
1 | |Throwable $line
23 | *
24 | * @return array
25 | */
26 | public static function encode($line): array
27 | {
28 | if ($line instanceof Throwable) {
29 | return [
30 | self::META_KEY => [
31 | self::TYPE_KEY => self::TYPE_THROWABLE,
32 | ],
33 | self::VALUE_KEY => throwable_encode($line),
34 | ];
35 | }
36 |
37 | $throwables = [];
38 | $line = Hash::flatten($line);
39 | foreach ($line as $key => $value) {
40 | if (! ($value instanceof Throwable)) {
41 | continue;
42 | }
43 |
44 | $throwables[] = $key;
45 | $line[$key] = throwable_encode($value);
46 | }
47 |
48 | return [
49 | self::META_KEY => [
50 | self::TYPE_KEY => self::TYPE_ARRAY,
51 | self::THROWABLES_KEY => $throwables,
52 | ],
53 | self::VALUE_KEY => $line,
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Messages/LineInterface.php:
--------------------------------------------------------------------------------
1 | $options
13 | */
14 | public function __construct(ActionableMessageInterface $line, array $options);
15 |
16 | public function __toString(): string;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Messages/Message.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
16 | }
17 |
18 | public function getPayload(): Payload
19 | {
20 | return $this->payload;
21 | }
22 |
23 | /**
24 | * @return array
25 | */
26 | public function jsonSerialize(): array
27 | {
28 | return [
29 | 'type' => 'message',
30 | 'payload' => $this->payload,
31 | ];
32 | }
33 |
34 | public function handle(object $bindTo, string $source): void
35 | {
36 | $cb = function (Payload $payload): void {
37 | /**
38 | * @psalm-suppress UndefinedMethod
39 | */
40 | $this->emit('message', [ /** @phpstan-ignore-line */
41 | $payload,
42 | $this,
43 | ]);
44 | };
45 | $cb = $cb->bindTo($bindTo);
46 | /**
47 | * @psalm-suppress PossiblyInvalidFunctionCall
48 | */
49 | $cb($this->payload);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Messages/Payload.php:
--------------------------------------------------------------------------------
1 | */
18 | protected array $payload = [];
19 |
20 | /**
21 | * @param array $payload
22 | *
23 | * @phpstan-ignore-next-line
24 | */
25 | public function __construct(array $payload = []) /** @phpstan-ignore-line */
26 | {
27 | $this->payload = $payload;
28 | }
29 |
30 | /**
31 | * @return array
32 | */
33 | public function getPayload(): array
34 | {
35 | return $this->payload;
36 | }
37 |
38 | /**
39 | * @return array
40 | */
41 | public function jsonSerialize(): array
42 | {
43 | return $this->payload;
44 | }
45 |
46 | /**
47 | * @param mixed|null $offset
48 | * @param mixed $value
49 | */
50 | public function offsetSet($offset, $value): void
51 | {
52 | if ($offset === null) {
53 | $this->payload[] = $value;
54 | } else {
55 | $this->payload[$offset] = $value;
56 | }
57 | }
58 |
59 | /**
60 | * @param mixed $offset
61 | */
62 | public function offsetExists($offset): bool
63 | {
64 | /** @phpstan-ignore-next-line */
65 | return isset($this->payload[$offset]);
66 | }
67 |
68 | /**
69 | * @param mixed $offset
70 | */
71 | public function offsetUnset($offset): void
72 | {
73 | unset($this->payload[$offset]);
74 | }
75 |
76 | /**
77 | * @param mixed $offset
78 | *
79 | * @return mixed|null
80 | */
81 | #[ReturnTypeWillChange]
82 | public function offsetGet($offset)
83 | {
84 | return $this->payload[$offset] ?? null;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Messages/Rpc.php:
--------------------------------------------------------------------------------
1 | target = $target;
26 | $this->payload = $payload;
27 | $this->uniqid = $uniqid;
28 | }
29 |
30 | public function getPayload(): Payload
31 | {
32 | return $this->payload;
33 | }
34 |
35 | public function setUniqid(string $uniqid): self
36 | {
37 | return new self($this->target, $this->payload, $uniqid);
38 | }
39 |
40 | /**
41 | * @return array
42 | */
43 | public function jsonSerialize(): array
44 | {
45 | return [
46 | 'type' => 'rpc',
47 | 'uniqid' => $this->uniqid,
48 | 'target' => $this->target,
49 | 'payload' => $this->payload,
50 | ];
51 | }
52 |
53 | public function handle(object $bindTo, string $source): void
54 | {
55 | $cb = Closure::fromCallable(function (string $target, Payload $payload, string $uniqid): void {
56 | /**
57 | * @psalm-suppress UndefinedMethod
58 | */
59 | if (! $this->hasRpc($target)) { /** @phpstan-ignore-line */
60 | $this->write($this->createLine(Factory::rpcError($uniqid, new Exception(sprintf('Rpc target <%s> doesn\'t exist', $target))))); /** @phpstan-ignore-line */
61 |
62 | return;
63 | }
64 |
65 | /**
66 | * @psalm-suppress UndefinedMethod
67 | */
68 | $this->callRpc($target, $payload)->done( /** @phpstan-ignore-line */
69 | function (array $payload) use ($uniqid): void {
70 | /**
71 | * @psalm-suppress UndefinedMethod
72 | */
73 | $this->write($this->createLine(Factory::rpcSuccess($uniqid, $payload))); /** @phpstan-ignore-line */
74 | },
75 | function (Throwable $error) use ($uniqid): void {
76 | /**
77 | * @psalm-suppress UndefinedMethod
78 | */
79 | $this->write($this->createLine(Factory::rpcError($uniqid, $error))); /** @phpstan-ignore-line */
80 | }
81 | );
82 | });
83 | $cb = $cb->bindTo($bindTo);
84 | /**
85 | * @psalm-suppress PossiblyInvalidFunctionCall
86 | */
87 | $cb($this->target, $this->payload, $this->uniqid);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Messages/RpcError.php:
--------------------------------------------------------------------------------
1 | uniqid = $uniqid;
22 | $this->payload = $payload;
23 | }
24 |
25 | /**
26 | * @return Exception|Throwable
27 | */
28 | public function getPayload()
29 | {
30 | return $this->payload;
31 | }
32 |
33 | /**
34 | * @return array
35 | */
36 | public function jsonSerialize(): array
37 | {
38 | return [
39 | 'type' => 'rpc_error',
40 | 'uniqid' => $this->uniqid,
41 | 'payload' => LineEncoder::encode($this->payload),
42 | ];
43 | }
44 |
45 | public function handle(MessengerInterface $bindTo, string $source): void
46 | {
47 | $cb = Closure::fromCallable(function (Throwable $payload, string $uniqid): void {
48 | /**
49 | * @psalm-suppress UndefinedMethod
50 | */
51 | $this->getOutstandingCall($uniqid)->reject($payload); /** @phpstan-ignore-line */
52 | });
53 | $cb = $cb->bindTo($bindTo);
54 | /**
55 | * @psalm-suppress PossiblyInvalidFunctionCall
56 | */
57 | $cb($this->payload, $this->uniqid);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Messages/RpcSuccess.php:
--------------------------------------------------------------------------------
1 | uniqid = $uniqid;
20 | $this->payload = $payload;
21 | }
22 |
23 | public function getPayload(): Payload
24 | {
25 | return $this->payload;
26 | }
27 |
28 | /**
29 | * @return array
30 | */
31 | public function jsonSerialize(): array
32 | {
33 | return [
34 | 'type' => 'rpc_success',
35 | 'uniqid' => $this->uniqid,
36 | 'payload' => $this->payload,
37 | ];
38 | }
39 |
40 | public function handle(MessengerInterface $bindTo, string $source): void
41 | {
42 | $cb = Closure::fromCallable(function (Payload $payload, string $uniqid): void {
43 | /**
44 | * @psalm-suppress UndefinedMethod
45 | */
46 | $this->getOutstandingCall($uniqid)->resolve($payload); /** @phpstan-ignore-line */
47 | });
48 | $cb = $cb->bindTo($bindTo);
49 | /**
50 | * @psalm-suppress PossiblyInvalidFunctionCall
51 | */
52 | $cb($this->payload, $this->uniqid);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Messages/SecureLine.php:
--------------------------------------------------------------------------------
1 | $options
23 | */
24 | public function __construct(ActionableMessageInterface $line, array $options)
25 | {
26 | $this->line = $line;
27 | $this->key = $options['key'];
28 | }
29 |
30 | public function __toString(): string
31 | {
32 | $line = json_encode($this->line);
33 |
34 | return json_encode([
35 | 'type' => 'secure',
36 | 'line' => $line,
37 | 'signature' => base64_encode(static::sign($line, $this->key)),
38 | ]) . LineInterface::EOL;
39 | }
40 |
41 | /**
42 | * @param array $line
43 | * @param array $lineOptions
44 | *
45 | * @throws Exception
46 | */
47 | public static function fromLine(array $line, array $lineOptions): ActionableMessageInterface
48 | {
49 | if (static::validate(base64_decode($line['signature'], true), $line['line'], $lineOptions['key'])) {
50 | return Factory::fromLine($line['line'], $lineOptions);
51 | }
52 |
53 | /** @phpstan-ignore-next-line */
54 | throw new Exception('Signature mismatch!');
55 | }
56 |
57 | private static function sign(string $line, string $key): string
58 | {
59 | return hash_hmac('sha256', $line, $key, true);
60 | }
61 |
62 | private static function validate(string $signature, string $line, string $key): bool
63 | {
64 | return hash_equals($signature, static::sign($line, $key));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Messenger.php:
--------------------------------------------------------------------------------
1 | */
41 | protected array $rpcs = [];
42 |
43 | /** @var array */
44 | protected array $options = [];
45 |
46 | protected string $buffer = '';
47 |
48 | /** @var array> */
49 | protected array $defaultOptions = [
50 | 'lineClass' => Line::class,
51 | 'messageFactoryClass' => MessagesFactory::class,
52 | 'lineOptions' => [],
53 | ];
54 |
55 | public function __construct(
56 | ConnectionInterface $connection,
57 | Options $options
58 | ) {
59 | $this->connection = $connection;
60 |
61 | /**
62 | * @psalm-suppress InvalidMethodCall
63 | */
64 | $this->options = $this->defaultOptions + $options->toArray();
65 |
66 | $this->outstandingRpcCalls = new OutstandingCalls();
67 |
68 | $this->connection->on('data', function (string $data): void {
69 | $this->buffer .= $data;
70 | $this->emit('data', [$data]);
71 | $this->handleData();
72 | });
73 | $this->connection->on('close', function (): void {
74 | $calls = $this->outstandingRpcCalls->getCalls();
75 | if (count($calls) === 0) {
76 | return;
77 | }
78 |
79 | $error = new CommunicationWithProcessUnexpectedEndException();
80 | $this->emit('error', [$error, $this]);
81 | foreach ($calls as $call) {
82 | $call->reject($error);
83 | }
84 | });
85 | }
86 |
87 | public function registerRpc(string $target, callable $listener): void
88 | {
89 | $this->rpcs[$target] = $listener;
90 | }
91 |
92 | public function deregisterRpc(string $target): void
93 | {
94 | unset($this->rpcs[$target]);
95 | }
96 |
97 | public function hasRpc(string $target): bool
98 | {
99 | return array_key_exists($target, $this->rpcs);
100 | }
101 |
102 | public function callRpc(string $target, Payload $payload): PromiseInterface
103 | {
104 | try {
105 | return $this->rpcs[$target]($payload, $this);
106 |
107 | /** @phpstan-ignore-next-line */
108 | } catch (Throwable $exception) {
109 | return reject($exception);
110 | }
111 | }
112 |
113 | public function message(Message $message): void
114 | {
115 | $this->write($this->createLine($message));
116 | }
117 |
118 | public function error(Error $error): void
119 | {
120 | $this->write($this->createLine($error));
121 | }
122 |
123 | public function getOutstandingCall(string $uniqid): OutstandingCallInterface
124 | {
125 | return $this->outstandingRpcCalls->getCall($uniqid);
126 | }
127 |
128 | public function rpc(Rpc $rpc): PromiseInterface
129 | {
130 | $callReference = $this->outstandingRpcCalls->newCall(function (): void {}); // phpcs:disabled
131 |
132 | $this->write($this->createLine($rpc->setUniqid($callReference->getUniqid())));
133 |
134 | return $callReference->getDeferred()->promise();
135 | }
136 |
137 | public function createLine(ActionableMessageInterface $line): string
138 | {
139 | $lineCLass = $this->options['lineClass'];
140 |
141 | /**
142 | * @psalm-suppress InvalidCast
143 | */
144 | return (string) new $lineCLass($line, $this->options['lineOptions']);
145 | }
146 |
147 | public function softTerminate(): PromiseInterface
148 | {
149 | return $this->rpc(MessagesFactory::rpc(self::TERMINATE_RPC));
150 | }
151 |
152 | public function write(string $line): void
153 | {
154 | $this->connection->write($line);
155 | }
156 |
157 | /**
158 | * @internal
159 | */
160 | public function crashed(int $exitCode): void
161 | {
162 | $this->emit('error', [new ProcessUnexpectedEndException($exitCode), $this]);
163 | }
164 |
165 | private function handleData(): void
166 | {
167 | if (strpos($this->buffer, LineInterface::EOL) === false) {
168 | return;
169 | }
170 |
171 | $messages = explode(LineInterface::EOL, $this->buffer);
172 | $this->buffer = array_pop($messages);
173 | $this->iterateMessages($messages);
174 | }
175 |
176 | /**
177 | * @param array $messages
178 | */
179 | private function iterateMessages(array $messages): void
180 | {
181 | foreach ($messages as $message) {
182 | try {
183 | MessagesFactory::fromLine($message, [])->handle($this, 'source');
184 | /** @phpstan-ignore-next-line */
185 | } catch (Throwable $exception) {
186 | $this->emit('error', [$exception, $this]);
187 | }
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/MessengerInterface.php:
--------------------------------------------------------------------------------
1 | emit('error', [new ProcessUnexpectedEndException($exitCode), $this]);
44 | // }
45 | }
46 |
--------------------------------------------------------------------------------
/src/OutstandingCall.php:
--------------------------------------------------------------------------------
1 | uniqid = $uniqid;
29 | $this->deferred = new Deferred($canceller);
30 |
31 | if (! is_callable($cleanup)) {
32 | $cleanup = static function (): void {
33 | };
34 | }
35 |
36 | $this->cleanup = $cleanup;
37 | }
38 |
39 | /**
40 | * @return mixed
41 | */
42 | public function getUniqid()
43 | {
44 | return $this->uniqid;
45 | }
46 |
47 | public function getDeferred(): Deferred
48 | {
49 | return $this->deferred;
50 | }
51 |
52 | /**
53 | * @param mixed $value
54 | */
55 | public function resolve($value): void
56 | {
57 | $cleanup = $this->cleanup;
58 | $cleanup($this);
59 |
60 | $this->deferred->resolve($value);
61 | }
62 |
63 | /**
64 | * @param mixed $value
65 | */
66 | public function reject($value): void
67 | {
68 | $cleanup = $this->cleanup;
69 | $cleanup($this);
70 |
71 | $this->deferred->reject($value);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/OutstandingCallInterface.php:
--------------------------------------------------------------------------------
1 | */
18 | protected array $calls = [];
19 |
20 | public function newCall(Closure $canceller): OutstandingCall
21 | {
22 | $uniqid = $this->getNewUniqid();
23 |
24 | $this->calls[$uniqid] = new OutstandingCall($uniqid, $canceller, function (OutstandingCall $call): void {
25 | unset($this->calls[$call->getUniqid()]);
26 | });
27 |
28 | return $this->calls[$uniqid];
29 | }
30 |
31 | public function getCall(string $uniqid): OutstandingCall
32 | {
33 | return $this->calls[$uniqid];
34 | }
35 |
36 | /**
37 | * @return array
38 | */
39 | public function getCalls(): array
40 | {
41 | return array_values($this->calls);
42 | }
43 |
44 | private function getNewUniqid(): string
45 | {
46 | do {
47 | $uniqid = (string) microtime(true) . '.' . bin2hex(random_bytes(4));
48 | } while (array_key_exists($uniqid, $this->calls));
49 |
50 | return $uniqid;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/ProcessUnexpectedEndException.php:
--------------------------------------------------------------------------------
1 | registerRpc('return', static function (Payload $payload): PromiseInterface {
20 | return resolve($payload->getPayload());
21 | });
22 | $messenger->on('message', static function (Payload $payload) use ($messenger): void {
23 | $messenger->message(MessagesFactory::message($payload->getPayload()));
24 | });
25 | }
26 |
27 | public static function create(Messenger $messenger, LoopInterface $loop): void
28 | {
29 | new static($messenger, $loop);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/StaticConfig.php:
--------------------------------------------------------------------------------
1 | getConstructor()->getParameters(); /** @phpstan-ignore-line */
27 | if (! isset($arguments[self::EXPECTED_INDEX])) { /** @phpstan-ignore-line */
28 | return $should = FALSE_;
29 | }
30 |
31 | return $should = ($arguments[self::EXPECTED_INDEX]->getName() === 'fds');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/var/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WyriHaximus/reactphp-child-process-messenger/937f23bebd38c95af54ec1be502bdcd7a58fdec2/var/.gitkeep
--------------------------------------------------------------------------------