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: 3 additions & 3 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -4689,9 +4689,9 @@ public static function worktreeAbandonedCleanup( array $input ): array|\WP_Error
public static function worktreeCleanupArtifacts( array $input ): array|\WP_Error {
$workspace = new Workspace();
$opts = array(
'dry_run' => ! empty($input['dry_run']),
'force' => ! empty($input['force']),
'allow_active_artifact_cleanup' => ! empty($input['allow_active_artifact_cleanup']),
'dry_run' => ! empty($input['dry_run']),
'force' => ! empty($input['force']),
'allow_active_artifact_cleanup' => ! empty($input['allow_active_artifact_cleanup']),
'allow_unavailable_process_probe' => ! empty($input['allow_unavailable_process_probe']),
);
if ( isset($input['apply_plan']) && is_array($input['apply_plan']) ) {
Expand Down
2 changes: 1 addition & 1 deletion inc/Cleanup/CleanupRemainingWorkSummary.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static function from_items( array $items ): array {
}

if ( 'artifact_cleanup' === $type && 'applied' !== $status ) {
$remaining = (array) ( $evidence['partial']['remaining_artifacts'] ?? array() );
$remaining = (array) ( $evidence['partial']['remaining_artifacts'] ?? array() );
$summary['remaining_reclaimable_artifact_bytes'] += 'partial' === $status
? array_sum(array_map(fn( $artifact ) => max(0, (int) ( $artifact['size_bytes'] ?? 0 )), $remaining))
: self::row_bytes($row, array( 'artifact_size_bytes', 'size_bytes' ));
Expand Down
6 changes: 3 additions & 3 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4325,9 +4325,9 @@ public function worktree( array $args, array $assoc_args ): void {
break;

case 'cleanup-artifacts':
$input['dry_run'] = ! empty($assoc_args['dry-run']);
$input['force'] = ! empty($assoc_args['force']);
$input['allow_active_artifact_cleanup'] = ! empty($assoc_args['allow-active-artifact-cleanup']);
$input['dry_run'] = ! empty($assoc_args['dry-run']);
$input['force'] = ! empty($assoc_args['force']);
$input['allow_active_artifact_cleanup'] = ! empty($assoc_args['allow-active-artifact-cleanup']);
$input['allow_unavailable_process_probe'] = ! empty($assoc_args['allow-unavailable-process-probe']);
if ( isset($assoc_args['limit']) ) {
$input['limit'] = (int) $assoc_args['limit'];
Expand Down
18 changes: 10 additions & 8 deletions inc/Storage/SqliteBusyRetry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

final class SqliteBusyRetry {

private const DEFAULT_MAX_WAIT_MS = 1000;
private const DEFAULT_MAX_WAIT_MS = 1000;
private const DEFAULT_INITIAL_WAIT_MS = 25;
private const DEFAULT_MAX_DELAY_MS = 250;
private const DEFAULT_MAX_DELAY_MS = 250;

/**
* Retry only a SQLite write which reports a transient busy/locked failure.
Expand Down Expand Up @@ -44,10 +44,11 @@ public static function run( string $operation_name, callable $operation ): mixed
throw $error;
}
$busy_message = $error->getMessage();
$result = false;
$result = false;
}

if ( false !== $result || ! self::is_busy_error($busy_message ?: (string) ( $wpdb->last_error ?? '' )) ) {
$last_error = '' !== $busy_message ? $busy_message : (string) ( $wpdb->last_error ?? '' );
if ( false !== $result || ! self::is_busy_error($last_error) ) {
return $result;
}

Expand All @@ -72,7 +73,7 @@ public static function run( string $operation_name, callable $operation ): mixed
$delay_ms = min($max_delay_ms, $initial_wait_ms * ( 2 ** ( $attempts - 1 ) ));
// Spread competing CLI processes without extending the configured budget.
$jitter_ms = $delay_ms > 1 ? random_int(0, max(1, (int) floor($delay_ms / 4))) : 0;
usleep((int) min($delay_ms + $jitter_ms, max(1, $max_wait_ms - $elapsed_ms)) * 1000);
usleep( (int) min($delay_ms + $jitter_ms, max(1, $max_wait_ms - $elapsed_ms)) * 1000);
} while ( true );
}

Expand All @@ -84,10 +85,11 @@ public static function is_sqlite( mixed $database ): bool {
return false;
}

$signals = array(get_class($database));
$signals = array( get_class($database) );
if ( method_exists($database, 'db_server_info') ) {
try {
$signals[] = (string) $database->db_server_info();
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- A failed optional capability probe is intentionally ignored.
} catch ( \Throwable ) {
// A failed capability probe must not alter normal database behavior.
}
Expand All @@ -112,8 +114,8 @@ private static function is_busy_error( string $message ): bool {
return str_contains($message, 'database is locked') || str_contains($message, 'database is busy') || str_contains($message, 'sqlite_busy') || str_contains($message, 'sqlite_locked');
}

private static function filtered_positive_int( string $hook, int $default ): int {
$value = function_exists('apply_filters') ? (int) apply_filters($hook, $default) : $default;
private static function filtered_positive_int( string $hook, int $default_value ): int {
$value = function_exists('apply_filters') ? (int) apply_filters($hook, $default_value) : $default_value;
return max(1, $value);
}
}
4 changes: 2 additions & 2 deletions inc/Storage/WorktreeInventoryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function last_error(): ?\WP_Error {
private function mutation_failure_error( string $operation ): \WP_Error {
global $wpdb;

$database_error = trim((string) ( $wpdb->last_error ?? '' ));
$database_error = trim( (string) ( $wpdb->last_error ?? '' ));
$database_error = SecretRedactor::redact($database_error);
$database_error = substr($database_error, 0, 512);

Expand Down Expand Up @@ -232,7 +232,7 @@ public function mark_missing( string $handle ): bool {
return false;
}

$data = array(
$data = array(
'missing_path' => 1,
'last_probe_at' => current_time('mysql', true),
'last_probe_status' => 'missing_path',
Expand Down
30 changes: 23 additions & 7 deletions inc/Support/ProcessPathProbe.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound -- The interface and its small provider implementations form one process-probe contract.
/**
* Portable process path probes.
*
Expand Down Expand Up @@ -28,7 +29,7 @@ final class ProcfsProcessPathProbe implements ProcessPathProbeInterface {
public function __construct(private $scanner) {}

public function snapshot(): array {
return ($this->scanner)();
return ( $this->scanner )();
}
}

Expand All @@ -40,7 +41,11 @@ public function snapshot(): array {
return array(
'status' => 'unsupported',
'records' => array(),
'diagnostics' => array( 'reason' => 'process_path_probe_unsupported', 'platform' => $this->platform, 'remediation' => 'Use a supported host process-path provider before artifact cleanup.' ),
'diagnostics' => array(
'reason' => 'process_path_probe_unsupported',
'platform' => $this->platform,
'remediation' => 'Use a supported host process-path provider before artifact cleanup.',
),
);
}
}
Expand All @@ -53,25 +58,33 @@ public function __construct(private $runner = null) {}
public function snapshot(): array {
$argv = array( 'lsof', '-n', '-P', '-Fpcfn0' );
if ( is_callable($this->runner) ) {
$result = ($this->runner)($argv);
$result = ( $this->runner )($argv);
} else {
$command = CommandSpec::from_argv($argv);
$result = $command instanceof \WP_Error ? $command : ProcessRunner::run($command, array( 'timeout_seconds' => 2, 'output_cap_bytes' => 1048576, 'error_as_result' => true ));
$result = $command instanceof \WP_Error ? $command : ProcessRunner::run($command, array(
'timeout_seconds' => 2,
'output_cap_bytes' => 1048576,
'error_as_result' => true,
));
}
if ( $result instanceof \WP_Error || ! is_array($result) || empty($result['success']) ) {
$data = $result instanceof \WP_Error ? (array) $result->get_error_data() : (array) $result;
return array(
'status' => 'uncertain',
'records' => array(),
'diagnostics' => array( 'reason' => ! empty($data['timeout']) ? 'process_path_probe_timeout' : 'process_path_probe_failed', 'provider' => 'lsof', 'details' => $data ),
'diagnostics' => array(
'reason' => ! empty($data['timeout']) ? 'process_path_probe_timeout' : 'process_path_probe_failed',
'provider' => 'lsof',
'details' => $data,
),
);
}

$records = array();
$pid = 0;
$command = '';
$fd = '';
foreach ( explode("\0", (string) ($result['output'] ?? '')) as $field ) {
foreach ( explode("\0", (string) ( $result['output'] ?? '' )) as $field ) {
$field = ltrim($field, "\n");
if ( '' === $field ) {
continue;
Expand All @@ -97,7 +110,10 @@ public function snapshot(): array {
return array(
'status' => 'available',
'records' => $records,
'diagnostics' => array( 'provider' => 'lsof', 'path_records' => count($records) ),
'diagnostics' => array(
'provider' => 'lsof',
'path_records' => count($records),
),
);
}
}
38 changes: 24 additions & 14 deletions inc/Support/ProcessRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private static function run_via_exec( string $command, array $options, int $outp
}

/**
* @param string|list<string> $command
* @param array<string,mixed> $options
* @param callable|null $on_output
* @param array<string,mixed>|null $env
Expand Down Expand Up @@ -146,7 +147,7 @@ private static function run_via_proc_open( string|array $command, array $options
}

if ( null !== $deadline && microtime(true) >= $deadline ) {
$remaining = self::terminate_timed_out_process($process, $pipes, $output, $stdout, $stderr, (int) ( $status['pid'] ?? 0 ), $uses_process_group);
$remaining = self::terminate_timed_out_process($process, $pipes, $output, $stdout, $stderr, (int) $status['pid'], $uses_process_group);
return self::error(
$options,
sprintf('Process command timed out after %d second(s).', $timeout_seconds),
Expand Down Expand Up @@ -250,10 +251,10 @@ private static function terminate_timed_out_process( $process, array $pipes, str
if ( self::is_windows() && $pid > 0 ) {
// taskkill's /T removes descendants that inherited the process pipes.
$taskkill = self::terminate_windows_process_tree($pid);
$cleanup = array(
'containment' => 'windows_process_tree',
'verified' => $taskkill['success'],
'attempts' => 1,
$cleanup = array(
'containment' => 'windows_process_tree',
'verified' => $taskkill['success'],
'attempts' => 1,
'taskkill_exit_code' => $taskkill['exit_code'],
);
} elseif ( $uses_process_group && $pid > 0 && function_exists('posix_kill') ) {
Expand Down Expand Up @@ -296,9 +297,9 @@ private static function terminate_timed_out_process( $process, array $pipes, str
proc_close($process);

return array(
'output' => $output,
'stdout' => $stdout,
'stderr' => $stderr,
'output' => $output,
'stdout' => $stdout,
'stderr' => $stderr,
'cleanup' => $cleanup,
);
}
Expand All @@ -307,19 +308,28 @@ private static function terminate_timed_out_process( $process, array $pipes, str
* Put timed POSIX commands in a session of their own so descendants cannot
* keep the command's pipes alive after the timeout owner exits.
*
* @param string|array<int,string> $command Command to execute.
* @return array{command: string|array<int,string>, uses_process_group: bool}
* @param string|list<string> $command Command to execute.
* @return array{command: string|list<string>, uses_process_group: bool}
*/
private static function command_with_process_group( string|array $command, int $timeout_seconds ): array {
if ( $timeout_seconds <= 0 || self::is_windows() || null === self::setsid_command() ) {
return array( 'command' => $command, 'uses_process_group' => false );
return array(
'command' => $command,
'uses_process_group' => false,
);
}

if ( is_array($command) ) {
return array( 'command' => array_merge(array( self::setsid_command() ), $command), 'uses_process_group' => true );
return array(
'command' => array_merge(array( self::setsid_command() ), $command),
'uses_process_group' => true,
);
}

return array( 'command' => array( self::setsid_command(), 'sh', '-c', $command ), 'uses_process_group' => true );
return array(
'command' => array( self::setsid_command(), 'sh', '-c', $command ),
'uses_process_group' => true,
);
}

private static function setsid_command(): ?string {
Expand All @@ -344,7 +354,7 @@ private static function terminate_windows_process_tree( int $pid ): array {
$output = array();
$exit_code = 1;
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec
@exec(sprintf('taskkill /PID %d /T /F', $pid), $output, $exit_code);
exec(sprintf('taskkill /PID %d /T /F', $pid), $output, $exit_code);

return array(
'success' => 0 === $exit_code,
Expand Down
Loading