Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs :

strategy :
matrix :
php_version : [ '8.1' ]
php_version : [ '8.1', '8.2', '8.3', '8.4' ]
steps :
- uses : actions/checkout@v2

Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ echo $process;
$process->getPayload();
```

### Tailing process logs

`tailProcessStdoutLog()` and `tailProcessStderrLog()` return a `TailLogInterface` object with the log
chunk, the next offset to request, and an overflow flag.

```php
$offset = 0;

do {
$tail = $supervisor->tailProcessStdoutLog('my_process', $offset, 4096);

echo $tail->getBytes();

$offset = $tail->getOffset();
} while ($tail->isOverflow());
```

> `FailedException` (fault 30) is thrown when the process log file does not exist yet, e.g. the
> process has never been started or was configured without a `stdout_logfile`. Wrap tail calls in a
> try/catch when the log may not exist yet.

### Exception handling

For each possible fault response there is an exception. These exceptions extend a [common exception](src/Exception/Fault.php), so you are able to catch a specific fault or all. When an unknown fault is returned from the server, an instance if the common exception is thrown. The list of fault responses and the appropriate exception can be found in the class.
Expand All @@ -110,10 +131,6 @@ try {
You can find the Supervisor XML-RPC documentation here:
[http://supervisord.org/api.html](http://supervisord.org/api.html)

## Notice

If you use PHP XML-RPC extension to parse responses (which is marked as *EXPERIMENTAL*). This can cause issues when you are trying to read/tail log of a PROCESS. Make sure you clean your log messages. The only information I found about this is a [comment](http://www.php.net/function.xmlrpc-decode#44213).

## Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"Supervisor\\": "src/"
}
},
"minimum-stability": "dev",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
Expand Down
6 changes: 6 additions & 0 deletions spec/ProcessSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ function it_checks_process_state()
$this->checkState(2)->shouldReturn(false);
}

function it_throws_when_accessing_unknown_key()
{
$this->shouldThrow(\OutOfBoundsException::class)
->duringOffsetGet('nonexistent_key');
}

function it_throws_an_exception_when_being_altered_by_calling_offset_set()
{
$this->shouldThrow(\LogicException::class)
Expand Down
65 changes: 65 additions & 0 deletions spec/ReloadResultSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace spec\Supervisor;

use PhpSpec\ObjectBehavior;
use Supervisor\ReloadResult;
use Supervisor\ReloadResultInterface;

class ReloadResultSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith(['added'], ['modified'], ['removed']);
}

function it_is_initializable()
{
$this->shouldHaveType(ReloadResult::class);
}

function it_implements_reload_result_interface()
{
$this->shouldImplement(ReloadResultInterface::class);
}

function it_returns_added_groups()
{
$this->getAdded()->shouldReturn(['added']);
}

function it_returns_modified_groups()
{
$this->getModified()->shouldReturn(['modified']);
}

function it_returns_removed_groups()
{
$this->getRemoved()->shouldReturn(['removed']);
}

function it_returns_all_affected_groups()
{
$this->getAffected()->shouldReturn(['added', 'modified', 'removed']);
}

function it_can_be_built_from_reload_config_response()
{
$this->beConstructedThrough('fromReloadConfig', [[
[['added_group'], ['modified_group'], ['removed_group']]
]]);

$this->getAdded()->shouldReturn(['added_group']);
$this->getModified()->shouldReturn(['modified_group']);
$this->getRemoved()->shouldReturn(['removed_group']);
}

function it_handles_empty_reload_config_response()
{
$this->beConstructedThrough('fromReloadConfig', [[]]);

$this->getAdded()->shouldReturn([]);
$this->getModified()->shouldReturn([]);
$this->getRemoved()->shouldReturn([]);
}
}
44 changes: 44 additions & 0 deletions spec/StateInfoSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace spec\Supervisor;

use PhpSpec\ObjectBehavior;
use Supervisor\ServiceStates;
use Supervisor\StateInfo;
use Supervisor\StateInfoInterface;

class StateInfoSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith(ServiceStates::Running, 'RUNNING');
}

function it_is_initializable()
{
$this->shouldHaveType(StateInfo::class);
}

function it_implements_state_info_interface()
{
$this->shouldImplement(StateInfoInterface::class);
}

function it_returns_state_code()
{
$this->getStateCode()->shouldReturn(ServiceStates::Running);
}

function it_returns_state_name()
{
$this->getStateName()->shouldReturn('RUNNING');
}

function it_can_be_built_from_get_state_response()
{
$this->beConstructedThrough('fromGetState', [['statecode' => 1, 'statename' => 'RUNNING']]);

$this->getStateCode()->shouldReturn(ServiceStates::Running);
$this->getStateName()->shouldReturn('RUNNING');
}
}
104 changes: 102 additions & 2 deletions spec/SupervisorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
use fXmlRpc\ClientInterface;
use PhpSpec\ObjectBehavior;
use Supervisor\Process;
use Supervisor\ReloadResult;
use Supervisor\ServiceStates;
use Supervisor\StateInfo;
use Supervisor\Supervisor;
use Supervisor\TailLog;

class SupervisorSpec extends ObjectBehavior
{
Expand Down Expand Up @@ -44,15 +48,15 @@ function it_calls_a_method(ClientInterface $client)
function it_checks_if_supervisor_is_running(ClientInterface $client)
{
$client->call('supervisor.getState', [])
->willReturn(['statecode' => 1]);
->willReturn(['statecode' => 1, 'statename' => 'RUNNING']);

$this->isRunning()->shouldReturn(true);
}

function it_checks_supervisor_state(ClientInterface $client)
{
$client->call('supervisor.getState', [])
->willReturn(['statecode' => 1]);
->willReturn(['statecode' => 1, 'statename' => 'RUNNING']);

$this->checkState(1)->shouldReturn(true);
}
Expand Down Expand Up @@ -83,4 +87,100 @@ function it_returns_a_process_(ClientInterface $client)
$process->shouldHaveType(Process::class);
$process->getName()->shouldReturn('process_name');
}

function it_returns_state(ClientInterface $client)
{
$client->call('supervisor.getState', [])
->willReturn(['statecode' => 1, 'statename' => 'RUNNING']);

$result = $this->getState();

$result->shouldHaveType(StateInfo::class);
$result->getStateCode()->shouldReturn(ServiceStates::Running);
$result->getStateName()->shouldReturn('RUNNING');
}

function it_tails_process_stdout_log(ClientInterface $client)
{
$client->call('supervisor.tailProcessStdoutLog', ['process_name', 0, 100])
->willReturn(['log content', 11, false]);

$result = $this->tailProcessStdoutLog('process_name', 0, 100);

$result->shouldHaveType(TailLog::class);
$result->getBytes()->shouldReturn('log content');
$result->getOffset()->shouldReturn(11);
$result->isOverflow()->shouldReturn(false);
}

function it_tails_process_stderr_log(ClientInterface $client)
{
$client->call('supervisor.tailProcessStderrLog', ['process_name', 0, 100])
->willReturn(['error content', 13, true]);

$result = $this->tailProcessStderrLog('process_name', 0, 100);

$result->shouldHaveType(TailLog::class);
$result->getBytes()->shouldReturn('error content');
$result->getOffset()->shouldReturn(13);
$result->isOverflow()->shouldReturn(true);
}

function it_removes_groups_on_reload(ClientInterface $client)
{
$client->call('supervisor.reloadConfig', [])->willReturn([[[], [], ['old_group']]]);
$client->call('supervisor.stopProcessGroup', ['old_group', true])->willReturn([]);
$client->call('supervisor.removeProcessGroup', ['old_group'])->willReturn(true);

$result = $this->reloadAndApplyConfig();

$result->shouldHaveType(ReloadResult::class);
$result->getRemoved()->shouldReturn(['old_group']);
}

function it_restarts_modified_groups_on_reload(ClientInterface $client)
{
$client->call('supervisor.reloadConfig', [])->willReturn([[[], ['changed_group'], []]]);
$client->call('supervisor.stopProcessGroup', ['changed_group', true])->willReturn([]);
$client->call('supervisor.removeProcessGroup', ['changed_group'])->willReturn(true);
$client->call('supervisor.addProcessGroup', ['changed_group'])->willReturn(true);
$client->call('supervisor.startProcessGroup', ['changed_group'])->willReturn([]);

$result = $this->reloadAndApplyConfig();

$result->shouldHaveType(ReloadResult::class);
$result->getModified()->shouldReturn(['changed_group']);
}

function it_does_not_stop_modified_groups_when_flag_is_false(ClientInterface $client)
{
$client->call('supervisor.reloadConfig', [])->willReturn([[[], ['changed_group'], []]]);
$client->call('supervisor.stopProcessGroup', ['changed_group', true])->shouldNotBeCalled();
$client->call('supervisor.removeProcessGroup', ['changed_group'])->shouldNotBeCalled();
$client->call('supervisor.addProcessGroup', ['changed_group'])->willReturn(true);
$client->call('supervisor.startProcessGroup', ['changed_group'])->willReturn([]);

$this->reloadAndApplyConfig(true, false, true);
}

function it_starts_added_groups_on_reload(ClientInterface $client)
{
$client->call('supervisor.reloadConfig', [])->willReturn([[['new_group'], [], []]]);
$client->call('supervisor.addProcessGroup', ['new_group'])->willReturn(true);
$client->call('supervisor.startProcessGroup', ['new_group'])->willReturn([]);

$result = $this->reloadAndApplyConfig();

$result->shouldHaveType(ReloadResult::class);
$result->getAdded()->shouldReturn(['new_group']);
}

function it_does_not_start_new_processes_when_flag_is_false(ClientInterface $client)
{
$client->call('supervisor.reloadConfig', [])->willReturn([[['new_group'], [], []]]);
$client->call('supervisor.addProcessGroup', ['new_group'])->willReturn(true);
$client->call('supervisor.startProcessGroup', ['new_group'])->shouldNotBeCalled();

$this->reloadAndApplyConfig(true, true, false);
}
}
56 changes: 56 additions & 0 deletions spec/TailLogSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace spec\Supervisor;

use PhpSpec\ObjectBehavior;
use Supervisor\TailLog;
use Supervisor\TailLogInterface;

class TailLogSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith('log content', 11, false);
}

function it_is_initializable()
{
$this->shouldHaveType(TailLog::class);
}

function it_implements_tail_log_interface()
{
$this->shouldImplement(TailLogInterface::class);
}

function it_returns_bytes()
{
$this->getBytes()->shouldReturn('log content');
}

function it_returns_offset()
{
$this->getOffset()->shouldReturn(11);
}

function it_returns_overflow_flag()
{
$this->isOverflow()->shouldReturn(false);
}

function it_can_be_built_from_tail_log_response()
{
$this->beConstructedThrough('fromTailLog', [['log content', 11, false]]);

$this->getBytes()->shouldReturn('log content');
$this->getOffset()->shouldReturn(11);
$this->isOverflow()->shouldReturn(false);
}

function it_casts_overflow_to_bool_when_built_from_response()
{
$this->beConstructedThrough('fromTailLog', [['data', 5, 1]]);

$this->isOverflow()->shouldReturn(true);
}
}
1 change: 1 addition & 0 deletions src/Exception/FaultCodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum FaultCodes: int
case StillRunning = 91;
case CantReread = 92;

/** @return class-string<SupervisorException> */
public function getExceptionClass(): string
{
return match($this) {
Expand Down
Loading