<commit_before><?php

namespace PHPCensor\Helper;

use Exception;
use PHPCensor\Logging\BuildLogger;
use Symfony\Component\Process\Process;

/**
 * Handles running system commands with variables.
 */
class CommandExecutor implements CommandExecutorInterface
{
    /**
     * @var BuildLogger
     */
    protected $logger;

    /**
     * @var bool
     */
    protected $verbose;

    /**
     * @var array
     */
    protected $lastOutput;

    /**
     * @var string
     */
    protected $lastError;

    /**
     * @var boolean
     */
    public $logExecOutput = true;

    /**
     * The path which findBinary will look in.
     *
     * @var string
     */
    protected $rootDir;

    /**
     * Current build path
     *
     * @var string
     */
    protected $buildPath;

    /**
     * Commands with no proper exit mechanism
     *
     * @var array
     */
    private static $noExitCommands = [
        'codecept',
    ];

    /**
     * @param BuildLogger $logger
     * @param string      $rootDir
     * @param bool        $verbose
     */
    public function __construct(BuildLogger $logger, $rootDir, $verbose = false)
    {
        $this->logger     = $logger;
        $this->verbose    = $verbose;
        $this->lastOutput = [];
        $this->rootDir    = $rootDir;
    }

    /**
     * Executes shell commands.
     *
     * @param array $args
     *
     * @return bool Indicates success
     */
    public function executeCommand($args = [])
    {
        $this->lastOutput = [];

        $this->logger->logDebug('Args: ' . json_encode($args));

        $command = call_user_func_array('sprintf', $args);

        $this->logger->logDebug('Command: ' . $command);

        $withNoExit = '';
        foreach (self::$noExitCommands as $nec) {
            if (preg_match("/\b{$nec}\b/", $command)) {
                $withNoExit = $nec;
                break;
            }
        }

        $process = new Process($command, $this->buildPath);
        $process->setTimeout(86400);

        if (!empty($withNoExit)) {
            $process->start();

            $this->logger->logDebug("Assuming command '{$withNoExit}' does not exit properly");
            do {
                sleep(15);
                $response = [];
                exec("ps auxww | grep '{$withNoExit}' | grep -v grep", $response);
                $response = array_filter(
                    $response,
                    function ($a) {
                        return strpos($a, $this->buildPath) !== false;
                    }
                );
            } while (!empty($response));
            $process->stop();
            $status = 0;
        } else {
            $process->setIdleTimeout(600);
            $process->start();
            $status = $process->wait();
        }

        $lastOutput = $this->replaceIllegalCharacters($process->getOutput());
        $lastError  = $this->replaceIllegalCharacters($process->getErrorOutput());

        $this->lastOutput = array_filter(explode(PHP_EOL, $lastOutput));
        $this->lastError  = $lastError;

        $shouldOutput = ($this->logExecOutput && ($this->verbose || 0 !== $status));

        if ($shouldOutput && !empty($this->lastOutput)) {
            $this->logger->log($this->lastOutput);
        }

        if (!empty($this->lastError)) {
            $this->logger->logFailure($this->lastError);
        }

        $isSuccess = false;
        if (0 === $status) {
            $isSuccess = true;
        }

        $this->logger->logDebug('Execution status: ' . $status);

        return $isSuccess;
    }

    /**
     * @param string $utf8String
     *
     * @return string
     */
    public function replaceIllegalCharacters($utf8String)
    {
        mb_substitute_character(0xFFFD); // is '�'
        $legalUtf8String = mb_convert_encoding($utf8String, 'utf8', 'utf8');
        $regexp          = '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]' .
            '|[^\x{0}-\x{ffff}]/u'; // more than 3 byte UTF-8 sequences (unsupported in mysql)<commit_msg>Reverted part of PR #250. Issue #261.<commit_after><?php

namespace PHPCensor\Helper;

use Exception;
use PHPCensor\Logging\BuildLogger;

/**
 * Handles running system commands with variables.
 */
class CommandExecutor implements CommandExecutorInterface
{
    /**
     * @var BuildLogger
     */
    protected $logger;

    /**
     * @var bool
     */
    protected $verbose;

    /**
     * @var array
     */
    protected $lastOutput;

    /**
     * @var string
     */
    protected $lastError;

    /**
     * @var boolean
     */
    public $logExecOutput = true;

    /**
     * The path which findBinary will look in.
     *
     * @var string
     */
    protected $rootDir;

    /**
     * Current build path
     *
     * @var string
     */
    protected $buildPath;

    /**
     * @param BuildLogger $logger
     * @param string      $rootDir
     * @param bool        $verbose
     */
    public function __construct(BuildLogger $logger, $rootDir, $verbose = false)
    {
        $this->logger     = $logger;
        $this->verbose    = $verbose;
        $this->lastOutput = [];
        $this->rootDir    = $rootDir;
    }
    /**
     * Executes shell commands.
     *
     * @param array $args
     *
     * @return bool Indicates success
     */
    public function executeCommand($args = [])
    {
        $this->lastOutput = [];

        $this->logger->logDebug('Args: ' . json_encode($args));

        $command = call_user_func_array('sprintf', $args);

        $this->logger->logDebug('Command: ' . $command);

        $status         = 0;
        $descriptorSpec = [
            0 => ["pipe", "r"], // stdin
            1 => ["pipe", "w"], // stdout
            2 => ["pipe", "w"], // stderr
        ];

        $pipes   = [];
        $process = proc_open($command, $descriptorSpec, $pipes, $this->buildPath, null);

        $lastOutput = '';
        $lastError  = '';
        if (is_resource($process)) {
            fclose($pipes[0]);

            list($lastOutput, $lastError) = $this->readAlternating([$pipes[1], $pipes[2]]);

            $status = (int)proc_close($process);

            $lastOutput = $this->replaceIllegalCharacters($lastOutput);
            $lastError  = $this->replaceIllegalCharacters($lastError);
        }

        $this->lastOutput = array_filter(explode(PHP_EOL, $lastOutput));
        $this->lastError  = $lastError;

        $shouldOutput = ($this->logExecOutput && ($this->verbose || 0 !== $status));

        if ($shouldOutput && !empty($this->lastOutput)) {
            $this->logger->log($this->lastOutput);
        }

        if (!empty($this->lastError)) {
            $this->logger->logFailure($this->lastError);
        }

        $isSuccess = false;
        if (0 === $status) {
            $isSuccess = true;
        }

        $this->logger->logDebug('Execution status: ' . $status);

        return $isSuccess;
    }

    /**
     * Reads from array of streams as data becomes available.
     *
     * @param array $descriptors
     *
     * @return string[] data read from each descriptor
     */
    private function readAlternating(array $descriptors)
    {
        $outputs = [];
        foreach ($descriptors as $key => $descriptor) {
            stream_set_blocking($descriptor, false);
            $outputs[$key] = '';
        }

        // 20 retries for 15 seconds = 5 min
        $retries = 20;
        $timeout = 15;
        do {
            $resources = 0;
            $read      = [];
            for ($i = 0; $i < $retries; ++$i) {
                $read      = $descriptors;
                $write     = null;
                $except    = null;
                $resources = stream_select($read, $write, $except, $timeout);
                if (intval($resources) > 0) {
                    break;
                }
            }
            foreach ($read as $descriptor) {
                $key = array_search($descriptor, $descriptors);
                if (feof($descriptor)) {
                    fclose($descriptor);
                    unset($descriptors[$key]);
                } else {
                    $buffer = fgets($descriptor);
                    if (false === $buffer) {
                        fclose($descriptor);
                        unset($descriptors[$key]);
                        continue;
                    }
                    $outputs[$key] .= $buffer;
                }
            }
        } while (count($descriptors) > 0 && intval($resources) > 0);

        return $outputs;
    }

    /**
     * @param string $utf8String
     *
     * @return string
     */
    public function replaceIllegalCharacters($utf8String)
    {
        mb_substitute_character(0xFFFD); // is '�'
        $legalUtf8String = mb_convert_encoding($utf8String, 'utf8', 'utf8');
        $regexp          = '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]' .
            '|[^\x{0}-\x{ffff}]/u'; // more than 3 byte UTF-8 sequences (unsupported in mysql)
{'fraction_non_alphanumeric': 0.14756111173122, 'fraction_numerical': 0.009599285634557428, 'mean_word_length': 1.6762246117084827, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 74, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3078679', 'n_tokens_mistral': 2674, 'n_tokens_neox': 2552, 'n_words': 525}