Skip to content
Merged
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
6 changes: 5 additions & 1 deletion inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
use DataMachineCode\Workspace\WorkspaceReader;
use DataMachineCode\Workspace\WorkspaceWriter;
use DataMachineCode\Support\GitRunner;
use DataMachineCode\Support\RuntimeCapabilities;

defined('ABSPATH') || exit;

if ( ! class_exists(AbilityRegistry::class) ) {
require_once __DIR__ . '/AbilityRegistry.php';
}
if ( ! class_exists(RuntimeCapabilities::class) ) {
require_once dirname(__DIR__) . '/Support/RuntimeCapabilities.php';
}

class WorkspaceAbilities {

Expand Down Expand Up @@ -2327,7 +2331,7 @@ public static function getCapabilities( array $input ): array { // phpcs:ignor
$workspace = new Workspace();
$diagnostic = GitRunner::diagnose();
$backend = RemoteWorkspaceBackend::should_handle() ? 'github_api' : 'local_git';
$remediation = 'Run workspace abilities in a host runtime with local git access, or provide a Data Machine Code workspace backend that executes these operations outside the constrained PHP sandbox.';
$remediation = RuntimeCapabilities::workspace_remediation();

return array_merge(
$diagnostic,
Expand Down
88 changes: 10 additions & 78 deletions inc/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,20 @@

namespace DataMachineCode;

use DataMachineCode\Support\RuntimeCapabilities;

if ( ! defined('ABSPATH') ) {
exit;
}

if ( ! class_exists(RuntimeCapabilities::class) ) {
require_once __DIR__ . '/Support/RuntimeCapabilities.php';
}

class Environment {



/**
* Memoized shell capability diagnostic. Null until the first probe.
*
* @var array{ok: bool, reason: string, output?: string, exit_code?: int|null}|null
*/
private static $shell_diagnostic_cache = null;

/**
* Is Data Machine Code available on this install?
*
Expand Down Expand Up @@ -85,33 +84,10 @@ public static function has_shell(): bool {
*
* @since 0.24.0
*
* @return array{ok: bool, reason: string, output?: string, exit_code?: int|null}
* @return array{ok: bool, reason: string, exec_available: bool, shell_exec_available: bool, proc_open_available: bool, output?: string, exit_code?: int|null}
*/
public static function shell_diagnostic(): array {
if ( null !== self::$shell_diagnostic_cache ) {
return self::$shell_diagnostic_cache;
}

self::$shell_diagnostic_cache = self::evaluate_shell_capability(
static function ( string $function_name ): bool {
return function_exists($function_name);
},
(string) ini_get('disable_functions'),
static function ( string $command ): array {
$output = array();
$exit_code = null;

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Environment capability probe.
exec($command, $output, $exit_code);

return array(
'output' => $output,
'exit_code' => $exit_code,
);
}
);

return self::$shell_diagnostic_cache;
return RuntimeCapabilities::shell_diagnostic();
}

/**
Expand All @@ -122,54 +98,10 @@ static function ( string $command ): array {
* @param callable $function_exists Callback receiving a function name and returning availability.
* @param string $disabled_functions Raw `disable_functions` value.
* @param callable $command_runner Callback receiving command and returning output plus exit code.
* @return array{ok: bool, reason: string, output?: string, exit_code?: int|null}
* @return array{ok: bool, reason: string, exec_available: bool, shell_exec_available: bool, proc_open_available: bool, output?: string, exit_code?: int|null}
*/
private static function evaluate_shell_capability( callable $function_exists, string $disabled_functions, callable $command_runner ): array {
$required_functions = array( 'exec', 'shell_exec' );
$disabled = array_filter(array_map('trim', explode(',', $disabled_functions)));

foreach ( $required_functions as $function_name ) {
if ( ! $function_exists($function_name) ) {
return array(
'ok' => false,
'reason' => $function_name . '_missing',
);
}

if ( in_array($function_name, $disabled, true) ) {
return array(
'ok' => false,
'reason' => $function_name . '_disabled',
);
}
}

$marker = '__datamachine_code_shell_ok__';

/**
* @var array{output: array<int, string>, exit_code: int|null} $result
*/
$result = $command_runner('printf ' . escapeshellarg($marker) . ' 2>&1');

$output = $result['output'];
$exit_code = $result['exit_code'];

$actual_output = trim(implode("\n", array_map('strval', $output)));
if ( 0 !== $exit_code || $marker !== $actual_output ) {
return array(
'ok' => false,
'reason' => 'probe_failed',
'output' => $actual_output,
'exit_code' => $exit_code,
);
}

return array(
'ok' => true,
'reason' => 'ok',
'output' => $actual_output,
'exit_code' => $exit_code,
);
return RuntimeCapabilities::evaluate_shell_capability($function_exists, $disabled_functions, $command_runner);
}

/**
Expand Down
39 changes: 5 additions & 34 deletions inc/Support/GitRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,21 @@
if ( ! class_exists(ProcessRunner::class) ) {
require_once __DIR__ . '/ProcessRunner.php';
}
if ( ! class_exists(RuntimeCapabilities::class) ) {
require_once __DIR__ . '/RuntimeCapabilities.php';
}

final class GitRunner {



/**
* Cached runtime capability probe.
*
* @var array<string,mixed>|null
*/
private static ?array $diagnostic = null;

/**
* Inspect whether the current PHP runtime can execute local git commands.
*
* @return array<string,mixed>
*/
public static function diagnose(): array {
if ( null !== self::$diagnostic ) {
return self::$diagnostic;
}

$exec_available = function_exists('exec');
$proc_open_available = function_exists('proc_open');
$output = array();
$exit_code = 127;
$git_path = '';

if ( $exec_available ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec
exec('command -v git 2>/dev/null', $output, $exit_code);
$git_path = trim( (string) ( $output[0] ?? '' ));
}

self::$diagnostic = array(
'backend' => 'local_git',
'exec_available' => $exec_available,
'proc_open_available' => $proc_open_available,
'git_available' => 0 === $exit_code && '' !== $git_path,
'git_path' => $git_path,
'probe_exit_code' => $exit_code,
);

return self::$diagnostic;
return RuntimeCapabilities::git_diagnostic();
}

/**
Expand Down Expand Up @@ -100,7 +71,7 @@ public static function unavailable_error( string $operation = 'Git workspace ope
$reason = 'PHP proc_open() is unavailable for streaming git operations.';
}

$remediation = 'Run workspace abilities in a host runtime with local git access, or provide a Data Machine Code workspace backend that executes these operations outside the constrained PHP sandbox.';
$remediation = RuntimeCapabilities::workspace_remediation();

return new \WP_Error(
'datamachine_workspace_git_unavailable',
Expand Down
16 changes: 10 additions & 6 deletions inc/Support/ProcessRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

defined('ABSPATH') || exit;

if ( ! class_exists(RuntimeCapabilities::class) ) {
require_once __DIR__ . '/RuntimeCapabilities.php';
}

final class ProcessRunner {


Expand All @@ -35,13 +39,12 @@ public static function run( string $command, array $options = array() ): array|\
return self::run_via_exec($command, $options, $output_cap);
}

if ( ! function_exists('proc_open') ) {
$shell = RuntimeCapabilities::shell_diagnostic();
if ( empty($shell['proc_open_available']) ) {
return self::error(
$options,
0 === $timeout_seconds ? 'Process command failed to start.' : sprintf('Process command timed out after %d second(s).', $timeout_seconds),
array(
'proc_open_available' => false,
)
$shell
);
}

Expand All @@ -53,8 +56,9 @@ public static function run( string $command, array $options = array() ): array|\
* @return array{success: bool, output: string, exit_code: int}|\WP_Error
*/
private static function run_via_exec( string $command, array $options, int $output_cap ): array|\WP_Error {
if ( ! function_exists('exec') ) {
return self::error($options, 'Process command failed to start.', array( 'exec_available' => false ));
$shell = RuntimeCapabilities::shell_diagnostic();
if ( empty($shell['exec_available']) ) {
return self::error($options, 'Process command failed to start.', $shell);
}

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec
Expand Down
Loading
Loading