diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5f5976..a78db1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: name: 'PHPUnit' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -26,7 +26,7 @@ jobs: dependency-versions: ${{ matrix.dependencies }} - name: PHPUnit run: php -dmemory_limit=-1 vendor/bin/phpunit --coverage-clover=coverage.clover - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} psalm: @@ -38,7 +38,7 @@ jobs: name: 'Psalm' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -58,7 +58,7 @@ jobs: name: 'CS' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index d3c5f3f..0000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Documentation - -on: - push: - branches: [master] - -jobs: - publish: - runs-on: ubuntu-latest - name: 'Publish documentation' - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - - name: Install halsey/journal - run: composer global require halsey/journal - - name: Generate - run: composer global exec 'journal generate' - - name: Push - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./.tmp_journal/ diff --git a/.journal b/.journal deleted file mode 100644 index 869510b..0000000 --- a/.journal +++ /dev/null @@ -1,65 +0,0 @@ -package('innmind', 'operating-system', null, 'OperatingSystem') - ->menu( - Entry::markdown( - 'Getting started', - Path::of('readme.md'), - ), - Entry::section( - 'Use cases', - Entry::markdown( - 'Manipulating time', - Path::of('use_cases/time.md'), - ), - Entry::markdown( - 'Filesystem', - Path::of('use_cases/filesystem.md'), - ), - Entry::markdown( - 'HTTP Client', - Path::of('use_cases/http.md'), - ), - Entry::markdown( - 'Processes', - Path::of('use_cases/processes.md'), - ), - Entry::markdown( - 'Inter Process Communication', - Path::of('use_cases/ipc.md'), - ), - Entry::markdown( - 'Socket communication', - Path::of('use_cases/socket.md'), - ), - Entry::markdown( - 'Handling process signals', - Path::of('use_cases/signals.md'), - ), - Entry::markdown( - 'SQL connection', - Path::of('use_cases/sql.md'), - ), - )->alwaysOpen(), - Entry::section( - 'Advanced usage', - Entry::markdown( - 'Logging all operations', - Path::of('advanced/logging.md'), - ), - Entry::markdown( - 'Extensions', - Path::of('advanced/extensions.md'), - ), - ), - ); -}; diff --git a/CHANGELOG.md b/CHANGELOG.md index e678341..c77e8ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 5.0.0 - 2024-03-10 + +### Added + +- `Innmind\OperatingSystem\Filesystem::temporary()` + +### Changed + +- `Innmind\OperatingSystem\Remote::socket()` returned socket is now wrapped in a `Innmind\IO\Sockets\Client` +- `Innmind\OperatingSystem\Sockets::connectTo()` returned socket is now wrapped in a `Innmind\IO\Sockets\Client` +- `Innmind\OperatingSystem\Sockets::open()` returned socket is now wrapped in a `Innmind\IO\Sockets\Server` +- `Innmind\OperatingSystem\Sockets::takeOver()` returned socket is now wrapped in a `Innmind\IO\Sockets\Server` +- `Innmind\OperatingSystem\Ports::open()` returned socket is now wrapped in a `Innmind\IO\Sockets\Server` +- `Innmind\OperatingSystem\CurrentProcess\Generic::of()` is now declared `internal` +- `Innmind\OperatingSystem\Filesystem\Generic::of()` is now declared `internal` +- `Innmind\OperatingSystem\Ports\Unix::of()` is now declared `internal` +- `Innmind\OperatingSystem\Remote\Generic::of()` is now declared `internal` +- `Innmind\OperatingSystem\Ports\Sockets::of()` is now declared `internal` +- Requires `innmind/file-watch:~4.0` + ## 4.2.0 - 2023-12-14 ### Added diff --git a/README.md b/README.md index 4fc0e3e..d3b5199 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Abstraction for most of the operating system the PHP code run on. The goal is to deal with the operating system in a more abstract way (instead of dealing with concrete, low level, details). -**Important**: you must use [`vimeo/psalm`](https://packagist.org/packages/vimeo/psalm) to make sure you use this library correctly. +> [!IMPORTANT] +> you must use [`vimeo/psalm`](https://packagist.org/packages/vimeo/psalm) to make sure you use this library correctly. ## Installation @@ -16,6 +17,10 @@ The goal is to deal with the operating system in a more abstract way (instead of composer require innmind/operating-system ``` +## Documentation + +Documentation is located in the [`documentation/`](documentation) folder. + ## Usage ```php @@ -70,7 +75,7 @@ $server = $os Port::of(1337), ) ->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => throw new \RuntimeException('Cannot open the socket'), ); ``` @@ -84,7 +89,7 @@ $server = $os use Innmind\Socket\Address\Unix; $server = $os->sockets()->open(Unix::of('/tmp/foo.sock'))->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => throw new \RuntimeException('Cannot open the socket'), ); ``` @@ -95,7 +100,10 @@ $server = $os->sockets()->open(Unix::of('/tmp/foo.sock'))->match( # process B use Innmind\Socket\Address\Unix; -$client = $os->sockets()->connectTo(Unix::of('/tmp/foo.sock')); +$client = $os->sockets()->connectTo(Unix::of('/tmp/foo.sock'))->match( + static fn($client) => $client->unwrap(), + static fn() => throw new \RuntimeException('Cannot connect to the socket'), +); ``` `$client` is an instance of `Innmind\Socket\Client`. diff --git a/composer.json b/composer.json index 5d3dd93..840dc7f 100644 --- a/composer.json +++ b/composer.json @@ -24,10 +24,10 @@ "innmind/http-transport": "~7.2", "innmind/time-warp": "~3.0", "innmind/signals": "~3.0", - "innmind/file-watch": "~3.1", + "innmind/file-watch": "~4.0", "innmind/stream": "~4.0", "formal/access-layer": "^2.0", - "innmind/io": "~2.2" + "innmind/io": "~2.7" }, "autoload": { "psr-4": { diff --git a/documentation/advanced/logging.md b/documentation/advanced/logging.md index b612492..28efa82 100644 --- a/documentation/advanced/logging.md +++ b/documentation/advanced/logging.md @@ -2,7 +2,8 @@ If you want to trace everything that is done on your operating system you can use the logger decorator that will automatically write to your log file (almost) all operations. -**Note**: data and actions done on a socket are not logged as well as processes output to prevent logging too much data (at least for now). +> [!NOTE] +> data and actions done on a socket are not logged as well as processes output to prevent logging too much data (at least for now). ```php use Innmind\OperatingSystem\OperatingSystem\Logger; diff --git a/documentation/readme.md b/documentation/readme.md index 38029bc..99c77ab 100644 --- a/documentation/readme.md +++ b/documentation/readme.md @@ -8,7 +8,8 @@ The other advantage to use higher level abstractions is to enable end user to bu For concrete examples have a look at the use cases available in the sidebar. -**Note**: this library is a small overlay on top of a set of individual libraries that contain the concrete abstractions. So you can start using only a subset of abstractions in your code as a starting point. +> [!NOTE] +> this library is a small overlay on top of a set of individual libraries that contain the concrete abstractions. So you can start using only a subset of abstractions in your code as a starting point. ## Installation @@ -26,4 +27,21 @@ $os = Factory::build(); There's nothing more to add to start using this abstraction. Head toward the use cases to understand all the things you can do with it. -**Note**: This library doesn't work on windows environments. +> [!WARNING] +> This library doesn't work on windows environments. + +## Use cases + +- [Manipulating time](use_cases/time.md) +- [Filesystem](use_cases/filesystem.md) +- [HTTP Client](use_cases/http.md) +- [Processes](use_cases/processes.md) +- [Inter Process Communication](use_cases/ipc.md) +- [Socket communication](use_cases/socket.md) +- [Handling process signals](use_cases/signals.md) +- [SQL connection](use_cases/sql.md) + +## Advanced usage + +- [Logging all operations](advanced/logging.md) +- [Extensions](advanced/extensions.md) diff --git a/documentation/use_cases/filesystem.md b/documentation/use_cases/filesystem.md index e83848c..f02e8b5 100644 --- a/documentation/use_cases/filesystem.md +++ b/documentation/use_cases/filesystem.md @@ -112,24 +112,24 @@ It is a great way to forget about where the tmp folder is located and simply foc A pattern we don't see much in PHP is an infinite loop to react to an event to perform another task. Here we can build such pattern by watching for changes in a file or a directory. ```php -use Innmind\FileWatch\Stop; -use Innmind\Immutable\Either; +use Innmind\FileWatch\Continuation; $runTests = $os->filesystem()->watch(Path::of('/path/to/project/src/')); -$count = $runTests(0, function(int $count) use ($os): Either { +$count = $runTests(0, function(int $count, Continuation $continuation) use ($os): Continuation { if ($count === 42) { - return Either::left(Stop::of($count)); + return $continuation->stop($count); } $os->control()->processes()->execute($phpunitCommand); - return Either::right(++$count); + return $continuation->continue(++$count); }); ``` -Here it will run phpunit tests every time the `src/` folder changes. Concrete examples of this pattern can be found in [`innmind/lab-station`](https://github.com/Innmind/LabStation/blob/develop/src/Agent/WatchSources.php#L38) to run a suite of tools when sources change or in [`halsey/journal`](https://github.com/halsey-php/journal/blob/develop/src/Command/Preview.php#L58) to rebuild the website when the markdown files change. +Here it will run phpunit tests every time the `src/` folder changes. Concrete examples of this pattern can be found in [`innmind/lab-station`](https://github.com/Innmind/LabStation/blob/develop/src/Agent/WatchSources.php#L38) to run a suite of tools when sources change. -This operation is a bit like an `array_reduce` as you can keep a state record between each calls of the callable via the first argument (here `0`, but it can be anything) and the argument of your callable will be the previous value returned by `Either::right()`. +This operation is a bit like an `array_reduce` as you can keep a state record between each calls of the callable via the first argument (here `0`, but it can be anything) and the argument of your callable will be the previous value returned by `$continuation->continue()`. -**Important**: since there is not builtin way to watch for changes in a directory it checks the directory every second, so use it with care. Watching an individual file is a bit safer as it uses the `tail` command so there is no `sleep()` used. +> [!WARNING] +> since there is no builtin way to watch for changes in a directory it checks the directory every second, so use it with care. Watching an individual file is a bit safer as it uses the `tail` command so there is no `sleep()` used. diff --git a/documentation/use_cases/http.md b/documentation/use_cases/http.md index 6673ef5..25b2e9d 100644 --- a/documentation/use_cases/http.md +++ b/documentation/use_cases/http.md @@ -28,7 +28,8 @@ $response instanceof Response; // true All elements of a request/response call is built using objects to enforce correctness of the formatted messages. -**Note**: since request and responses messages can be viewed either from a client or a server the model is abstracted in the standalone [`innmind/http` library](https://github.com/innmind/http). +> [!NOTE] +> since request and responses messages can be viewed either from a client or a server the model is abstracted in the standalone [`innmind/http` library](https://github.com/innmind/http). ## Resiliency in a distributed system @@ -62,4 +63,5 @@ $response = $http($request); $response = $http($request); ``` -**Note**: the circuit breaker works on host per host basis meaning if `server1.com` fails then calls to `server2.com` will still be sent. +> [!NOTE] +> the circuit breaker works on host per host basis meaning if `server1.com` fails then calls to `server2.com` will still be sent. diff --git a/documentation/use_cases/ipc.md b/documentation/use_cases/ipc.md index d78c5f7..f73a02c 100644 --- a/documentation/use_cases/ipc.md +++ b/documentation/use_cases/ipc.md @@ -4,13 +4,17 @@ To communicate between processes on a same system there is 2 approaches: sharing The later is the safest of the two (but not exempt of problems) and you will find here the building blocks to communicate via a socket. -**Note**: the adage `share state through messages and not messages through state` is a pillar of the [actor model](https://en.wikipedia.org/wiki/Actor_model) and [initially of object oriented programming](https://www.youtube.com/watch?v=7erJ1DV_Tlo). +> [!TIP] +> the adage `share state through messages and not messages through state` is a pillar of the [actor model](https://en.wikipedia.org/wiki/Actor_model) and [initially of object oriented programming](https://www.youtube.com/watch?v=7erJ1DV_Tlo). ```php # process acting as a server use Innmind\Socket\Address\Unix as Address; use Innmind\TimeContinuum\Earth\ElapsedPeriod; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Sequence, + Str, +}; $server = $os->sockets()->open(Address::of('/tmp/foo'))->match( static fn($server) => $server, @@ -19,45 +23,43 @@ $server = $os->sockets()->open(Address::of('/tmp/foo'))->match( $watch = $os->sockets()->watch(new ElapsedPeriod(1000))->forRead($server); while (true) { - $watch() - ->flatMap(static fn($ready) => $ready->toRead()->find(static fn($socket) => $socket === $stream)) - ->flatMap(static fn($server) => $server->accept()) + $_ = $server + ->timeoutAfter(ElapsedPeriod::of(1_000)) + ->accept() ->match( static fn($client) => $client - ->write(Str::of('Hello 👋')) - ->flatMap(static fn($client) => $client->close()) + ->send(Sequence::of(Str::of('Hello'))) + ->flatMap(static fn() => $client->close()) ->match( static fn() => null, // everyhting is ok static fn() => throw new \RuntimeException('Unable to send data or close the connection'), ), static fn() => null, // no new connection available - ); + ), } ``` ```php # process acting as client use Innmind\Socket\Address\Unix as Address; -use Innmind\TimeContinuum\Earth\ElapsedPeriod; - -$client = $os->sockets()->connectTo(Address::of('/tmp/foo')); -$watch = $os->sockets()->watch(new ElapsedPeriod(1000))->forRead($client); +use Innmind\IO\Readable\Frame; -do { - $ready = $watch() - ->flatMap(static fn($ready) => $ready->toRead()->find(static fn($ready) => $ready === $client)) - ->match( - static fn() => true, - static fn() => false, - ); -} while (!$ready); - -echo $client->read()->match( - static fn($data) => $data->toString(), - static fn() => 'unable to read the stream', +$client = $os->sockets()->connectTo(Address::of('/tmp/foo'))->match( + static fn($client) => $client, + static fn() => throw new \RuntimeException('Unable to connect to the server'), ); + +echo $client + ->watch() + ->frames(Frame\Chunk::of(5)) + ->one() + ->match( + static fn($data) => $data->toString(), + static fn() => 'unable to read the stream', + ); ``` -In the case the server is started first then the client would print `Hello 👋`. +In the case the server is started first then the client would print `Hello`. -**Important**: this is a very rough implementation of communication between processes. **DO NOT** use this simple implementation in your code, instead use a higher level API such as [`innmind/ipc`](https://github.com/innmind/ipc). +> [!WARNING] +> this is a very rough implementation of communication between processes. **DO NOT** use this simple implementation in your code, instead use a higher level API such as [`innmind/ipc`](https://github.com/innmind/ipc). diff --git a/documentation/use_cases/signals.md b/documentation/use_cases/signals.md index 5983dfa..e882981 100644 --- a/documentation/use_cases/signals.md +++ b/documentation/use_cases/signals.md @@ -2,7 +2,7 @@ Any process can receive [signals](https://en.wikipedia.org/wiki/Signal_(IPC)) either through user interaction (in a terminal), from another process or via the `kill` command. PHP processes can handle them and perform actions to safely free resources or prevent the process from being terminated. -Examples below only use one listener per signal but you can add as many as you want (which is complicated when dealing manually with PHP builtin functions). +Examples below only use one listener per signal but you can add as many as you wish (which is complicated when dealing manually with PHP builtin functions). ## Free resources before stopping @@ -10,38 +10,50 @@ This is a reuse of the [socket example](socket.md). ```php use Innmind\Url\Url; +use Innmind\IO\Readable\Frame; use Innmind\Socket\Internet\Transport; use Innmind\TimeContinuum\Earth\ElapsedPeriod; use Innmind\Signals\Signal; +use Innmind\Immutable\{ + Sequence, + Str, +}; $client = $os->remote()->socket(Transport::tcp(), Ur::of('tcp://127.0.0.1:8080')->authority())->match( static fn($client) => $client, static fn() => throw new \RuntimeException('Unable to connect to the server'), ); $watch = $os->sockets()->watch(new ElapsedPeriod(1000))->forRead($client); -$continue = true; -$os->process()->signals()->listen(Signal::terminate, function() use (&$continue, $client) { - $continue = false; - $client->close(); +$signaled = true; +$os->process()->signals()->listen(Signal::terminate, function() use (&$signaled) { + $signaled = false; }); -do { - $ready = $watch() - ->flatMap(static fn($ready) => $ready->toRead()->find(static fn($ready) => $ready === $client)) - ->match( - static fn() => true, - static fn() => false, - ); -} while ($continue && !$ready); - -if (!$client->closed()) { +$receivedData = $client + ->timeoutAfter(ElapsedPeriod::of(1_000)) + // it sends this every second to keep the connection alive + ->heartbeatWith(static fn() => Sequence::of(Str::of('foo'))) + ->abortWhen(function() use (&$signaled) { + return $signaled; + }) + ->frames(Frame\Chunk::of(1)) + ->one() + ->match( + static fn() => true, + static fn() => false, + ); + +if ($receivedData) { echo 'Server has responded'. } + +$client->unwrap()->close(); ``` When the process receive the `SIGTERM` signal it will be paused then the anonymous function will be called and the process will then be resumed. -**Note**: signal handling is already performed when using [`innmind/ipc`](https://github.com/innmind/ipc) or [`innmind/amqp`](https://github.com/innmind/amqp) so you don't have to think about it. +> [!NOTE] +> signal handling is already performed when using [`innmind/ipc`](https://github.com/innmind/ipc) or [`innmind/amqp`](https://github.com/innmind/amqp) so you don't have to think about it. ## Prevent process from being stopped diff --git a/documentation/use_cases/socket.md b/documentation/use_cases/socket.md index 926db73..c70ba5b 100644 --- a/documentation/use_cases/socket.md +++ b/documentation/use_cases/socket.md @@ -21,12 +21,11 @@ $server = $os->ports()->open(Transport::tcp(), IPv4::localhost(), Port::of(8080) static fn($server) => $server, static fn() => throw new \RuntimeException('Unable to start the server'), ); -$watch = $os->sockets()->watch(new ElapsedPeriod(1000))->forRead($server); while (true) { - $watch() - ->flatMap(static fn($ready) => $ready->toRead()->find(static fn($ready) => $ready === $server)) - ->flatMap(static fn($server) => $server->accept()) + $server + ->timeoutAfter(ElapsedPeriod::of(1_000)) + ->accept() ->match( static fn($client) => /* talk to the client */, static fn() => null, // no client available yet @@ -42,23 +41,24 @@ This example will open a connection to the server defined above but can be chang ```php use Innmind\Url\Url; +use Innmind\IO\Readable\Frame; use Innmind\Socket\Internet\Transport; -use Innmind\TimeContinuum\Earth\ElapsedPeriod; -$client = $os->remote()->socket(Transport::tcp(), Ur::of('tcp://127.0.0.1:8080')->authority())->match( +$client = $os->remote()->socket(Transport::tcp(), Url::of('tcp://127.0.0.1:8080')->authority())->match( static fn($client) => $client, static fn() => throw new \RuntimeException('Unable to connect to the client'), ); -$watch = $os->sockets()->watch(new ElapsedPeriod(1000))->forRead($client); -do { - $ready = $watch() - ->flatMap(static fn($ready) => $ready->toRead()->find(static fn($ready) => $ready === $client)) - ->match( - static fn() => true, - static fn() => false, - ); -} while (!$ready); +$receivedData = $client + ->watch() + ->frames(Frame\Chunk::of(1)) + ->one() + ->match( + static fn() => true, + static fn() => false, + ); -echo 'Server has responded'. +if ($receivedData) { + echo 'Server has responded'. +} ``` diff --git a/documentation/use_cases/time.md b/documentation/use_cases/time.md index 4d78835..ee32e9c 100644 --- a/documentation/use_cases/time.md +++ b/documentation/use_cases/time.md @@ -2,7 +2,8 @@ Directly accessing time in a PHP code is straightforward (either via `DateTime` or time functions) but it prevents you to build testable code or require to use some hard to understand hacks. Instead it is simpler to think of time as another dependency that you need to inject in your code, thus easier to change the implementation when testing. -**Note** for a more in length presentation of why directly accessing time is problematic you can watch this [talk](https://www.youtube.com/watch?v=T_I6HhP9-6w) (in french). +> [!TIP] +> for a more in length presentation of why directly accessing time is problematic you can watch this [talk](https://www.youtube.com/watch?v=T_I6HhP9-6w) (in french). ## Accessing time diff --git a/src/Config.php b/src/Config.php index 9aa7adc..25bb955 100644 --- a/src/Config.php +++ b/src/Config.php @@ -74,7 +74,10 @@ public static function of(): self default => $streams->watch()->timeoutAfter($timeout), }), new Halt\Usleep, - EnvironmentPath::of(\getenv('PATH') ?: ''), + EnvironmentPath::of(match ($path = \getenv('PATH')) { + false => '', + default => $path, + }), $maxHttpConcurrency, $httpHeartbeat, false, diff --git a/src/CurrentProcess/Generic.php b/src/CurrentProcess/Generic.php index e82a738..fd81712 100644 --- a/src/CurrentProcess/Generic.php +++ b/src/CurrentProcess/Generic.php @@ -9,11 +9,6 @@ use Innmind\TimeContinuum\Period; use Innmind\TimeWarp\Halt; use Innmind\Signals\Handler; -use Innmind\Immutable\{ - Set, - Either, - SideEffect, -}; final class Generic implements CurrentProcess { @@ -25,6 +20,9 @@ private function __construct(Halt $halt) $this->halt = $halt; } + /** + * @internal + */ public static function of(Halt $halt): self { return new self($halt); diff --git a/src/Factory.php b/src/Factory.php index 2fa92cd..e3973b2 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -3,11 +3,6 @@ namespace Innmind\OperatingSystem; -use Innmind\TimeContinuum\{ - Clock, - Earth, -}; - final class Factory { public static function build(Config $config = null): OperatingSystem diff --git a/src/Filesystem.php b/src/Filesystem.php index e10d738..15fce46 100644 --- a/src/Filesystem.php +++ b/src/Filesystem.php @@ -3,10 +3,17 @@ namespace Innmind\OperatingSystem; -use Innmind\Filesystem\Adapter; +use Innmind\Filesystem\{ + Adapter, + File\Content, +}; use Innmind\Url\Path; use Innmind\FileWatch\Ping; -use Innmind\Immutable\Maybe; +use Innmind\Immutable\{ + Maybe, + Str, + Sequence, +}; interface Filesystem { @@ -18,4 +25,18 @@ public function contains(Path $path): bool; */ public function require(Path $path): Maybe; public function watch(Path $path): Ping; + + /** + * This method is to be used to generate a temporary file content even if it + * doesn't fit in memory. + * + * Usually the sequence of chunks comes from reading a socket meaning it + * can't be read twice. By using this temporary file content you can read it + * multiple times. + * + * @param Sequence> $chunks + * + * @return Maybe + */ + public function temporary(Sequence $chunks): Maybe; } diff --git a/src/Filesystem/Generic.php b/src/Filesystem/Generic.php index 40033b4..ad9c176 100644 --- a/src/Filesystem/Generic.php +++ b/src/Filesystem/Generic.php @@ -7,7 +7,10 @@ Filesystem, Config, }; -use Innmind\Filesystem\Adapter; +use Innmind\Filesystem\{ + Adapter, + File\Content, +}; use Innmind\Url\Path; use Innmind\Server\Control\Server\Processes; use Innmind\FileWatch\{ @@ -15,7 +18,13 @@ Factory, Watch, }; -use Innmind\Immutable\Maybe; +use Innmind\Stream\Bidirectional; +use Innmind\Immutable\{ + Maybe, + Sequence, + Str, + Predicate\Instance, +}; final class Generic implements Filesystem { @@ -32,6 +41,9 @@ private function __construct(Processes $processes, Config $config) $this->mounted = new \WeakMap; } + /** + * @internal + */ public static function of(Processes $processes, Config $config): self { return new self($processes, $config); @@ -96,4 +108,27 @@ public function watch(Path $path): Ping { return ($this->watch)($path); } + + public function temporary(Sequence $chunks): Maybe + { + $temporary = $this + ->config + ->streamCapabilities() + ->temporary() + ->new(); + + $temporary = $chunks->reduce( + Maybe::just($temporary), + static fn(Maybe $temporary, $chunk) => Maybe::all($temporary, $chunk)->flatMap( + static fn(Bidirectional $temporary, Str $chunk) => $temporary + ->write($chunk->toEncoding(Str\Encoding::ascii)) + ->maybe() + ->keep(Instance::of(Bidirectional::class)), + ), + ); + + return $temporary + ->map($this->config->io()->readable()->wrap(...)) + ->map(Content::io(...)); + } } diff --git a/src/Filesystem/Logger.php b/src/Filesystem/Logger.php index d934424..e8205ee 100644 --- a/src/Filesystem/Logger.php +++ b/src/Filesystem/Logger.php @@ -4,10 +4,16 @@ namespace Innmind\OperatingSystem\Filesystem; use Innmind\OperatingSystem\Filesystem; -use Innmind\Filesystem\Adapter; +use Innmind\Filesystem\{ + Adapter, + File\Content, +}; use Innmind\Url\Path; use Innmind\FileWatch\Ping; -use Innmind\Immutable\Maybe; +use Innmind\Immutable\{ + Maybe, + Sequence, +}; use Psr\Log\LoggerInterface; final class Logger implements Filesystem @@ -73,4 +79,16 @@ public function watch(Path $path): Ping $this->logger, ); } + + public function temporary(Sequence $chunks): Maybe + { + return $this + ->filesystem + ->temporary($chunks) + ->map(function(Content $content) { + $this->logger->debug('Temporary file created'); + + return $content; + }); + } } diff --git a/src/OperatingSystem/Unix.php b/src/OperatingSystem/Unix.php index 6ee0505..fcc5768 100644 --- a/src/OperatingSystem/Unix.php +++ b/src/OperatingSystem/Unix.php @@ -81,7 +81,7 @@ public function control(): ServerControl public function ports(): Ports { - return $this->ports ??= Ports\Unix::of(); + return $this->ports ??= Ports\Unix::of($this->config); } public function sockets(): Sockets diff --git a/src/Ports.php b/src/Ports.php index ce94c45..c46e89f 100644 --- a/src/Ports.php +++ b/src/Ports.php @@ -4,10 +4,8 @@ namespace Innmind\OperatingSystem; use Innmind\Url\Authority\Port; -use Innmind\Socket\{ - Internet\Transport, - Server, -}; +use Innmind\IO\Sockets\Server; +use Innmind\Socket\Internet\Transport; use Innmind\IP\IP; use Innmind\Immutable\Maybe; diff --git a/src/Ports/Logger.php b/src/Ports/Logger.php index b706522..5d8fed4 100644 --- a/src/Ports/Logger.php +++ b/src/Ports/Logger.php @@ -5,10 +5,7 @@ use Innmind\OperatingSystem\Ports; use Innmind\Url\Authority\Port; -use Innmind\Socket\{ - Internet\Transport, - Server, -}; +use Innmind\Socket\Internet\Transport; use Innmind\IP\IP; use Innmind\Immutable\Maybe; use Psr\Log\LoggerInterface; diff --git a/src/Ports/Unix.php b/src/Ports/Unix.php index f6a668c..a3f8bbb 100644 --- a/src/Ports/Unix.php +++ b/src/Ports/Unix.php @@ -3,29 +3,41 @@ namespace Innmind\OperatingSystem\Ports; -use Innmind\OperatingSystem\Ports; +use Innmind\OperatingSystem\{ + Ports, + Config, +}; use Innmind\Url\Authority\Port; +use Innmind\IO\Sockets\Server; use Innmind\Socket\{ Internet\Transport, - Server, + Server\Internet, }; use Innmind\IP\IP; use Innmind\Immutable\Maybe; final class Unix implements Ports { - private function __construct() + private Config $config; + + private function __construct(Config $config) { + $this->config = $config; } - public static function of(): self + /** + * @internal + */ + public static function of(Config $config): self { - return new self; + return new self($config); } public function open(Transport $transport, IP $ip, Port $port): Maybe { /** @var Maybe */ - return Server\Internet::of($transport, $ip, $port); + return Internet::of($transport, $ip, $port)->map( + $this->config->io()->sockets()->servers()->wrap(...), + ); } } diff --git a/src/Remote.php b/src/Remote.php index 2d65a09..afcd615 100644 --- a/src/Remote.php +++ b/src/Remote.php @@ -4,10 +4,8 @@ namespace Innmind\OperatingSystem; use Innmind\Server\Control\Server; -use Innmind\Socket\{ - Internet\Transport, - Client, -}; +use Innmind\Socket\Internet\Transport; +use Innmind\IO\Sockets\Client; use Innmind\Url\{ Url, Authority, diff --git a/src/Remote/Generic.php b/src/Remote/Generic.php index 841f1eb..2d40b67 100644 --- a/src/Remote/Generic.php +++ b/src/Remote/Generic.php @@ -39,6 +39,9 @@ private function __construct(Server $server, Config $config) $this->config = $config; } + /** + * @internal + */ public static function of(Server $server, Config $config): self { return new self($server, $config); @@ -62,8 +65,9 @@ public function ssh(Url $server): Server public function socket(Transport $transport, Authority $authority): Maybe { - /** @var Maybe */ - return Client\Internet::of($transport, $authority); + return Client\Internet::of($transport, $authority)->map( + $this->config->io()->sockets()->clients()->wrap(...), + ); } public function http(): HttpTransport diff --git a/src/Remote/Logger.php b/src/Remote/Logger.php index 2015162..0ac5253 100644 --- a/src/Remote/Logger.php +++ b/src/Remote/Logger.php @@ -5,10 +5,7 @@ use Innmind\OperatingSystem\Remote; use Innmind\Server\Control; -use Innmind\Socket\{ - Internet\Transport, - Client, -}; +use Innmind\Socket\Internet\Transport; use Innmind\Url\{ Url, Authority, diff --git a/src/Remote/Resilient.php b/src/Remote/Resilient.php index 0b3f3c0..c9d6131 100644 --- a/src/Remote/Resilient.php +++ b/src/Remote/Resilient.php @@ -8,10 +8,7 @@ CurrentProcess, }; use Innmind\Server\Control\Server; -use Innmind\Socket\{ - Internet\Transport, - Client, -}; +use Innmind\Socket\Internet\Transport; use Innmind\Url\{ Url, Authority, diff --git a/src/Sockets.php b/src/Sockets.php index 8a7c94a..3d9ffa2 100644 --- a/src/Sockets.php +++ b/src/Sockets.php @@ -3,11 +3,11 @@ namespace Innmind\OperatingSystem; -use Innmind\Socket\{ - Address\Unix, - Server, +use Innmind\IO\Sockets\{ Client, + Server, }; +use Innmind\Socket\Address\Unix; use Innmind\TimeContinuum\ElapsedPeriod; use Innmind\Stream\Watch; use Innmind\Immutable\Maybe; diff --git a/src/Sockets/Logger.php b/src/Sockets/Logger.php index 6e8c284..4ce3a25 100644 --- a/src/Sockets/Logger.php +++ b/src/Sockets/Logger.php @@ -4,11 +4,7 @@ namespace Innmind\OperatingSystem\Sockets; use Innmind\OperatingSystem\Sockets; -use Innmind\Socket\{ - Address\Unix as Address, - Server, - Client, -}; +use Innmind\Socket\Address\Unix as Address; use Innmind\TimeContinuum\ElapsedPeriod; use Innmind\Stream\Watch; use Innmind\Immutable\Maybe; diff --git a/src/Sockets/Unix.php b/src/Sockets/Unix.php index 2129398..3b29b63 100644 --- a/src/Sockets/Unix.php +++ b/src/Sockets/Unix.php @@ -25,6 +25,9 @@ private function __construct(Config $config) $this->config = $config; } + /** + * @internal + */ public static function of(Config $config): self { return new self($config); @@ -32,17 +35,23 @@ public static function of(Config $config): self public function open(Address $address): Maybe { - return Server\Unix::of($address); + return Server\Unix::of($address)->map( + $this->config->io()->sockets()->servers()->wrap(...), + ); } public function takeOver(Address $address): Maybe { - return Server\Unix::recoverable($address); + return Server\Unix::recoverable($address)->map( + $this->config->io()->sockets()->servers()->wrap(...), + ); } public function connectTo(Address $address): Maybe { - return Client\Unix::of($address); + return Client\Unix::of($address)->map( + $this->config->io()->sockets()->clients()->wrap(...), + ); } public function watch(ElapsedPeriod $timeout = null): Watch diff --git a/tests/Filesystem/GenericTest.php b/tests/Filesystem/GenericTest.php index f72be3a..0d4ad6b 100644 --- a/tests/Filesystem/GenericTest.php +++ b/tests/Filesystem/GenericTest.php @@ -8,13 +8,24 @@ Filesystem, Config, }; -use Innmind\Filesystem\Adapter\Filesystem as FilesystemAdapter; +use Innmind\Filesystem\{ + Adapter\Filesystem as FilesystemAdapter, + File\Content, +}; use Innmind\Url\Path; use Innmind\Server\Control\Server\Processes; use Innmind\FileWatch\Ping; use Fixtures\Innmind\Url\Path as FPath; +use Innmind\Immutable\{ + Sequence, + Str, + Maybe, +}; use PHPUnit\Framework\TestCase; -use Innmind\BlackBox\PHPUnit\BlackBox; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, + Set, +}; class GenericTest extends TestCase { @@ -120,4 +131,61 @@ public function testRequireFile() static fn() => null, )); } + + public function testCreateTemporaryFile() + { + $this + ->forAll(Set\Sequence::of(Set\Unicode::strings())) + ->then(function($chunks) { + $filesystem = Generic::of( + $this->createMock(Processes::class), + Config::of(), + ); + + $content = $filesystem + ->temporary( + Sequence::of(...$chunks) + ->map(Str::of(...)) + ->map(Maybe::just(...)), + ) + ->match( + static fn($content) => $content, + static fn() => null, + ); + + $this->assertInstanceOf(Content::class, $content); + $this->assertSame( + \implode('', $chunks), + $content->toString(), + ); + }); + } + + public function testCreateTemporaryFileFailure() + { + $this + ->forAll( + Set\Sequence::of(Set\Unicode::strings())->between(0, 20), // upper bound to fit in memory + Set\Sequence::of(Set\Unicode::strings())->between(0, 20), // upper bound to fit in memory + ) + ->then(function($leading, $trailing) { + $filesystem = Generic::of( + $this->createMock(Processes::class), + Config::of(), + ); + + $content = $filesystem + ->temporary( + Sequence::of(...[...$leading, null, ...$trailing]) + ->map(Maybe::of(...)) + ->map(static fn($chunk) => $chunk->map(Str::of(...))), + ) + ->match( + static fn($content) => $content, + static fn() => null, + ); + + $this->assertNull($content); + }); + } } diff --git a/tests/OperatingSystem/LoggerTest.php b/tests/OperatingSystem/LoggerTest.php index 1c1719a..c2cb5c2 100644 --- a/tests/OperatingSystem/LoggerTest.php +++ b/tests/OperatingSystem/LoggerTest.php @@ -18,15 +18,9 @@ use Innmind\Server\Control; use Psr\Log\LoggerInterface; use PHPUnit\Framework\TestCase; -use Innmind\BlackBox\{ - PHPUnit\BlackBox, - Set, -}; class LoggerTest extends TestCase { - use BlackBox; - private OperatingSystem $os; private OperatingSystem $underlying; diff --git a/tests/Ports/UnixTest.php b/tests/Ports/UnixTest.php index 320f12a..b0e0e4f 100644 --- a/tests/Ports/UnixTest.php +++ b/tests/Ports/UnixTest.php @@ -6,6 +6,7 @@ use Innmind\OperatingSystem\{ Ports\Unix, Ports, + Config, }; use Innmind\Socket\{ Internet\Transport, @@ -19,19 +20,19 @@ class UnixTest extends TestCase { public function testInterface() { - $this->assertInstanceOf(Ports::class, Unix::of()); + $this->assertInstanceOf(Ports::class, Unix::of(Config::of())); } public function testOpen() { - $ports = Unix::of(); + $ports = Unix::of(Config::of()); $socket = $ports->open( Transport::tlsv12(), IPv4::localhost(), Port::of(1234), )->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => null, ); diff --git a/tests/Remote/GenericTest.php b/tests/Remote/GenericTest.php index 303466b..2d735c4 100644 --- a/tests/Remote/GenericTest.php +++ b/tests/Remote/GenericTest.php @@ -102,7 +102,7 @@ public function testSocket() ); $socket = $remote->socket(Transport::tcp(), Url::of('tcp://127.0.0.1:1234')->authority())->match( - static fn($client) => $client, + static fn($client) => $client->unwrap(), static fn() => null, ); diff --git a/tests/Sockets/UnixTest.php b/tests/Sockets/UnixTest.php index 069f8dc..0d14379 100644 --- a/tests/Sockets/UnixTest.php +++ b/tests/Sockets/UnixTest.php @@ -29,7 +29,7 @@ public function testOpen() $sockets = Unix::of(Config::of()); $socket = $sockets->open(Address::of('/tmp/foo'))->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => null, ); @@ -48,11 +48,11 @@ public function testTakeOver() $sockets = Unix::of(Config::of()); $socket = $sockets->open(Address::of('/tmp/foo'))->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => null, ); $socket2 = $sockets->takeOver(Address::of('/tmp/foo'))->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => null, ); @@ -66,11 +66,11 @@ public function testConnectTo() $sockets = Unix::of(Config::of()); $server = $sockets->open(Address::of('/tmp/foo'))->match( - static fn($server) => $server, + static fn($server) => $server->unwrap(), static fn() => null, ); $client = $sockets->connectTo(Address::of('/tmp/foo'))->match( - static fn($client) => $client, + static fn($client) => $client->unwrap(), static fn() => null, );