Вход Регистрация
Файл: vendor/laravel/prompts/src/Task.php
Строк: 305
<?php

namespace LaravelPrompts;

use 
Closure;
use 
LaravelPromptsSupportLogger;
use 
LaravelPromptsThemesDefaultConcernsInteractsWithStrings;
use 
RuntimeException;

class 
Task extends Prompt
{
    use 
InteractsWithStrings;

    
/**
     * The minimum width for the longest line calculation.
     */
    
protected int $minWidth = 0;

    
/**
     * How long to wait between rendering each frame.
     */
    
public int $interval = 100;

    
/**
     * The number of times the task has been rendered.
     */
    
public int $count = 0;

    
/**
     * Whether the task can only be rendered once.
     */
    
public bool $static = false;

    
/**
     * The process ID after forking.
     */
    
protected int $pid;

    
/**
     * The socket for IPC communication.
     *
     * @var resource|null
     */
    
protected $socket;

    
/**
     * Pre-wrapped log lines for the scrolling output area.
     *
     * @var array<int, string>
     */
    
public array $logs = [];

    
/**
     * Stable status messages (success, warning, error).
     *
     * @var list<array{type: string, message: string}>
     */
    
public array $stableMessages = [];

    
/**
     * The maximum number of stable messages to display.
     */
    
public int $maxStableMessages = 10;

    
/**
     * The identifier for the task.
     */
    
public string $identifier = '';

    
/**
     * Whether the task has finished.
     */
    
public bool $finished = false;

    
/**
     * Buffer for incomplete lines from non-blocking socket reads.
     */
    
protected string $buffer = '';

    
/**
     * The log index where the current partial started, or null if not streaming.
     */
    
protected ?int $partialStartIndex = null;

    
/**
     * Create a new Task instance.
     */
    
public function __construct(
        public 
string $label = '',
        public 
int $limit = 10,
        public 
bool $keepSummary = false,
        public ?
string $subLabel = null,
    ) {
        
$this->identifier = uniqid();
    }

    
/**
     * Render the task and execute the callback.
     *
     * @template TReturn of mixed
     *
     * @param  Closure(Logger): TReturn  $callback
     * @return TReturn
     */
    
public function run(Closure $callback): mixed
    
{
        
$this->limit = min($this->limit, $this->terminal()->lines() - 10);
        
$this->recalculateMaxStableMessages();

        
$this->capturePreviousNewLines();

        if (! (
function_exists('pcntl_fork') && function_exists('posix_kill'))) {
            return 
$this->renderStatically($callback);
        }

        
$originalAsync = pcntl_async_signals(true);

        
pcntl_signal(SIGINT, fn () => exit());

        try {
            
$this->hideCursor();
            
$this->render();

            
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

            if (
$sockets === false) {
                return 
$this->renderStatically($callback);
            }

            
$this->pid = pcntl_fork();

            if (
$this->pid === 0) {
                
fclose($sockets[1]);
                
$childSocket = $sockets[0];
                
stream_set_blocking($childSocket, false);

                while (
true) { // @phpstan-ignore-line
                    
$this->receiveMessages($childSocket);

                    if (! 
$this->finished) {
                        
$this->render();
                        
$this->count++;
                    }

                    
usleep($this->interval * 1000);
                }
            } else {
                
fclose($sockets[0]);
                
$this->socket = $sockets[1];

                
$logger = new Logger($this->identifier, $this->socket);
                
$result = $callback($logger);

                if (
$this->socket !== null) {
                    
// Send a reset message to the parent process to reset the terminal.
                    
fwrite($this->socket, $this->identifier.'_'.'reset:'.($originalAsync ? 1 : 0).PHP_EOL);
                    
usleep($this->interval * 2000);
                }

                return 
$result;
            }
        } catch (
Throwable $e) {
            
$this->resetTerminal($originalAsync);

            throw 
$e;
        }
    }

    
/**
     * Receive and process messages from the parent process.
     *
     * @param  resource  $socket
     */
    
protected function receiveMessages($socket): void
    
{
        
$prefix = preg_quote($this->identifier, '/');

        while ((
$data = fgets($socket)) !== false) {
            
// Buffer incomplete lines from non-blocking reads.
            
if (! str_ends_with($data, PHP_EOL)) {
                
$this->buffer .= $data;

                continue;
            }

            
$line = rtrim($this->buffer.$data, PHP_EOL);
            
$this->buffer = '';

            if (
$line === '') {
                continue;
            }

            
// Check for typed messages: {id}_{type}:{content}
            
if (preg_match('/^'.$prefix.'_(success|warning|error|label|sublabel|reset|partial|commitpartial):(.*)/', $line, $matches)) {
                
$type = $matches[1];
                
$content = $matches[2];

                if (
$type === 'reset') {
                    
$this->resetTerminal((bool) $content);

                    continue;
                }

                if (
$type === 'partial') {
                    
$this->replacePartialLines($content);

                    continue;
                }

                if (
$type === 'commitpartial') {
                    
$this->partialStartIndex = null;

                    continue;
                }

                if (
$type === 'label') {
                    
$this->label = $content;
                } elseif (
$type === 'sublabel') {
                    
$this->subLabel = $content;
                    
$this->recalculateMaxStableMessages();
                } else {
                    
$this->stableMessages[] = ['type' => $type, 'message' => $content];
                    
$this->logs = [];
                    
$this->partialStartIndex = null;
                }

                while (
count($this->stableMessages) > $this->maxStableMessages) {
                    
array_shift($this->stableMessages);
                }

                continue;
            }

            
// Regular log line — strip cursor-reset control sequences.
            
$line = preg_replace('/e[(?:1)?Ge[2K/', '', $line);

            
// Wrap and add to ring buffer.
            
$this->addLogLines($line);
        }
    }

    
/**
     * Wrap a log line and append to the ring buffer, trimming to the limit.
     */
    
protected function addLogLines(string $line): void
    
{
        
$width = $this->terminal()->cols() - 10;
        
$plainText = $this->stripEscapeSequences($line);

        if (
mb_strwidth($plainText) > $width) {
            
$wrapped = $this->ansiWordwrap($line, $width);
        } else {
            
$wrapped = [$line];
        }

        
array_push($this->logs, ...$wrapped);

        while (
count($this->logs) > $this->limit) {
            
array_shift($this->logs);
        }
    }

    
/**
     * Replace the in-progress partial lines with the full accumulated text.
     */
    
protected function replacePartialLines(string $text): void
    
{
        if (
$this->partialStartIndex === null) {
            
$this->partialStartIndex = count($this->logs);
        }

        
// Truncate back to where the partial started.
        
$this->logs = array_slice($this->logs, 0, $this->partialStartIndex);

        
// Wrap and append the full accumulated partial text.
        
$width = $this->terminal()->cols() - 10;
        
$plainText = $this->stripEscapeSequences($text);

        if (
mb_strwidth($plainText) > $width) {
            
$wrapped = $this->ansiWordwrap($text, $width);
        } else {
            
$wrapped = [$text];
        }

        
array_push($this->logs, ...$wrapped);

        while (
count($this->logs) > $this->limit) {
            
array_shift($this->logs);
            
$this->partialStartIndex = max(0, $this->partialStartIndex - 1);
        }
    }

    
/**
     * Recompute the stable-message budget based on the current sub-label state.
     */
    
protected function recalculateMaxStableMessages(): void
    
{
        
$reserved = 2 + ($this->subLabel !== null && $this->subLabel !== '' ? 1 : 0);
        
$this->maxStableMessages = max(0, $this->terminal()->lines() - 10 - $this->limit - $reserved);
    }

    
/**
     * Reset the terminal.
     */
    
protected function resetTerminal(bool $originalAsync): void
    
{
        
$this->finished = true;

        
pcntl_async_signals($originalAsync);
        
pcntl_signal(SIGINT, SIG_DFL);

        if (
$this->socket !== null) {
            
fclose($this->socket);
            
$this->socket = null;
        }

        if (
$this->keepSummary && count($this->stableMessages) > 0) {
            
$this->render();

            return;
        }

        
$this->eraseRenderedLines();
    }

    
/**
     * Render a static version of the task.
     *
     * @template TReturn of mixed
     *
     * @param  Closure(Logger): TReturn  $callback
     * @return TReturn
     */
    
protected function renderStatically(Closure $callback): mixed
    
{
        
$this->static = true;

        try {
            
$this->hideCursor();
            
$this->render();

            
$logger = new Logger($this->identifier);
            
$result = $callback($logger);
        } finally {
            
$this->eraseRenderedLines();
        }

        return 
$result;
    }

    
/**
     * Disable prompting for input.
     *
     * @throws RuntimeException
     */
    
public function prompt(): never
    
{
        throw new 
RuntimeException('Task cannot be prompted.');
    }

    
/**
     * Get the current value of the prompt.
     */
    
public function value(): bool
    
{
        return 
true;
    }

    
/**
     * Clear the lines rendered by the task.
     */
    
protected function eraseRenderedLines(): void
    
{
        
$lines = explode(PHP_EOL, $this->prevFrame);
        
$this->moveCursor(-999, -count($lines) + 1);
        
$this->eraseDown();
    }

    
/**
     * Clean up after the task.
     */
    
public function __destruct()
    {
        if (! empty(
$this->pid)) {
            
posix_kill($this->pid, SIGHUP);
        }

        
parent::__destruct();
    }
}
Онлайн: 3
Реклама