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
83 changes: 81 additions & 2 deletions src/Cli/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace GT\Cron\Cli;

use DateTime;
use DateTimeZone;
use Gt\Cli\Argument\ArgumentValueList;
use Gt\Cli\Command\Command;
use Gt\Cli\Parameter\NamedParameter;
Expand All @@ -16,6 +17,8 @@
class RunCommand extends Command {
/** @SuppressWarnings(PHPMD.ExitExpression) */
public function run(?ArgumentValueList $arguments = null):int {
$this->applySystemTimezone();

$filename = $arguments->get("file", "crontab");
$filePath = implode(DIRECTORY_SEPARATOR, [
getcwd(),
Expand Down Expand Up @@ -85,6 +88,7 @@ public function cronRunStep(
?string $nextCommand = null
):void {
$now = new DateTime();
$this->stream->writeLine("Current time: " . $this->formatLocalTime($now));

if(is_null($wait)) {
$this->writeLine("No tasks in crontab.");
Expand All @@ -106,9 +110,9 @@ function(string $command):string {

$this->stream->writeLine($message);

$message = "Next job at: " . $wait->format("H:i:s");
$message = "Next job at: " . $this->formatLocalTime($wait);
if($nextCommand) {
$message .= " (" . $this->displayCommandName($nextCommand) . ")";
$message .= " [" . $this->displayCommandName($nextCommand) . "]";
}

if($now->diff($wait)->format("%a") > 0) {
Expand Down Expand Up @@ -139,6 +143,81 @@ protected function displayCommandName(string $command):string {
return basename(str_replace("\\", "/", $script));
}

protected function applySystemTimezone():void {
if($timezone = $this->detectSystemTimezone()) {
date_default_timezone_set($timezone);
}
}

protected function detectSystemTimezone():?string {
return $this->detectTimezoneFromEnvironment()
?? $this->detectTimezoneFromLocaltime()
?? $this->detectTimezoneFromTimezoneFile();
}

protected function detectTimezoneFromEnvironment():?string {
$environmentTimezone = getenv("TZ");
if($environmentTimezone !== false
&& $this->isValidTimezone($environmentTimezone)) {
return $environmentTimezone;
}

return null;
}

protected function detectTimezoneFromLocaltime():?string {
$localtimePath = "/etc/localtime";
if(is_link($localtimePath)) {
$link = readlink($localtimePath);
if($link !== false
&& preg_match("#/zoneinfo/(.+)$#", $link, $match)
&& $this->isValidTimezone($match[1])) {
return $match[1];
}
}

return null;
}

protected function detectTimezoneFromTimezoneFile():?string {
$timezonePath = "/etc/timezone";
if(is_file($timezonePath)) {
$timezone = file_get_contents($timezonePath);
if($timezone === false) {
return null;
}

$timezone = trim($timezone);
if($this->isValidTimezone($timezone)) {
return $timezone;
}
}

return null;
}

protected function isValidTimezone(string $timezone):bool {
return in_array(
$timezone,
DateTimeZone::listIdentifiers(),
true
);
}

protected function formatLocalTime(DateTime $dateTime):string {
$local = clone $dateTime;
$local->setTimezone(new DateTimeZone(date_default_timezone_get()));
$message = $local->format("H:i:s");

if($local->getOffset() !== 0) {
$utc = clone $local;
$utc->setTimezone(new DateTimeZone("UTC"));
$message .= " (" . $utc->format("H:i:s") . " UTC)";
}

return $message;
}

public function getName():string {
return "run";
}
Expand Down
129 changes: 121 additions & 8 deletions src/GoFunctionExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Gt\Config\ConfigFactory;
use Gt\ServiceContainer\Container;
use Gt\ServiceContainer\Injector;
use ReflectionClass;

class GoFunctionExecutor {
private ?Config $config = null;
Expand All @@ -15,15 +16,49 @@ public function __construct(
}

public function execute(CronScript $cronScript):void {
$config = $this->loadConfig();
$this->setupProjectAutoloader($config);
$this->withProjectDirectory(function() use($cronScript):void {
$this->loadProjectAutoloader();

$container = $this->createContainer($config);
$container->set(Input::fromQuery($cronScript->getQuery()));
$config = $this->loadConfig();
$this->setupProjectAutoloader($config);

$injector = new Injector($container);
$functionName = $this->loadGoFunction($cronScript->getPath());
$injector->invoke(null, $functionName);
$container = $this->createContainer($config);
$container->set(Input::fromQuery($cronScript->getQuery()));

$injector = new Injector($container);
$functionName = $this->loadGoFunction($cronScript->getPath());
$this->withProjectDirectory(
fn() => $injector->invoke(null, $functionName)
);
});
}

private function withProjectDirectory(callable $callback):void {
$originalDirectory = getcwd();
if(is_dir($this->projectDirectory)) {
chdir($this->projectDirectory);
}

try {
$callback();
}
finally {
if($originalDirectory !== false && is_dir($originalDirectory)) {
chdir($originalDirectory);
}
}
}

private function loadProjectAutoloader():void {
$autoloadPath = implode(DIRECTORY_SEPARATOR, [
$this->projectDirectory,
"vendor",
"autoload.php",
]);

if(is_file($autoloadPath)) {
require_once $autoloadPath;
}
}

private function loadConfig():Config {
Expand Down Expand Up @@ -137,8 +172,86 @@ private function loadGoFunction(string $path):string {
$code = preg_replace('/^\xEF\xBB\xBF/', '', $code) ?? $code;
$code = preg_replace('/^\s*<\?(php)?/i', '', $code, 1) ?? $code;
$code = preg_replace('/\?>\s*$/', '', $code, 1) ?? $code;
$code = $this->replaceMagicConstants($code, $path);

eval("namespace $namespace;\n" . $code);
eval("namespace $namespace;\n" . $this->getInternalAliasStatements($code) . $code);
return $functionName;
}

private function getInternalAliasStatements(string $code):string {
$aliasList = [];
$existingAliasList = $this->getExistingUseAliasList($code);
foreach([
...get_declared_classes(),
...get_declared_interfaces(),
...get_declared_traits(),
] as $className) {
$reflection = new ReflectionClass($className);
if(!$reflection->isInternal()) {
continue;
}

$shortName = $reflection->getShortName();
if(isset($existingAliasList[strtolower($shortName)])) {
continue;
}

$aliasList[$shortName] = "use \\" . ltrim($className, "\\") . ";\n";
}

ksort($aliasList);
return implode("", $aliasList);
}

/** @return array<string, true> */
private function getExistingUseAliasList(string $code):array {
$aliasList = [];
preg_match_all(
'/^\s*use\s+(?!function\b|const\b)([^;]+);/mi',
$code,
$matches
);

foreach($matches[1] as $useStatement) {
foreach(explode(",", $useStatement) as $usePart) {
$usePart = trim($usePart);
if(preg_match('/\s+as\s+(\w+)$/i', $usePart, $aliasMatch)) {
$alias = $aliasMatch[1];
}
else {
$alias = basename(str_replace("\\", "/", $usePart));
}

$aliasList[strtolower($alias)] = true;
}
}

return $aliasList;
}

private function replaceMagicConstants(string $code, string $path):string {
$file = var_export($path, true);
$directory = var_export(dirname($path), true);
$output = "";

foreach(token_get_all("<?php\n" . $code) as $index => $token) {
if($index === 0) {
continue;
}

if(!is_array($token)) {
$output .= $token;
continue;
}

$output .= match($token[0]) {
T_FILE => $file,
T_DIR => $directory,
T_NAME_QUALIFIED => "\\" . $token[1],
default => $token[1],
};
}

return $output;
}
}
Loading
Loading