1: <?php
2: if(php_sapi_name() !== "cli"){
3: echo "This script can only be run from the command line.\n";
4: exit;
5: }
6:
7: if(PHP_VERSION_ID < 80000){
8: echo "ERROR: PHP Version 8 or newer is required to run PHP-CLI\n";
9: sleep(10);
10: exit;
11: }
12: if(PHP_VERSION_ID < 80400){
13: echo "Warning: PHP Version 8.4 or newer is recommended\n";
14: }
15:
16: if(!in_array(PHP_OS_FAMILY, ['Windows','Linux'])){
17: echo "This can only be run on Windows and some Linux distributions.\n";
18: exit;
19: }
20:
21: //Logs setup
22: if(!is_dir('logs')){
23: if(!mkdir('logs',0777)){
24: echo "Error: Unable to create logs directory\n";
25: }
26: }
27: if(is_file('logs/latest.log')){
28: if(!unlink('logs/latest.log')){
29: echo "Warning: Unable to delete old latest.log\n";
30: }
31: }
32:
33: mklog(1,'Starting');
34:
35: mklog(1,'Loading resources');
36: require_once 'resource2.php';
37:
38: if(is_admin::check()){
39: mklog(1,"Starting with administrator permissions");
40: }
41:
42: mklog(1,'Reading start arguments');
43:
44: if(!isset($fileArguments)){
45: $fileArguments = [];
46: }
47: if(!is_array($fileArguments) || array_is_list($fileArguments)){
48: mklog(2,'Unknown fileArguments configuration, ignoring all');
49: $fileArguments = [];
50: }
51:
52: $arguments = (function(){
53: global $argv;
54: global $fileArguments;
55:
56: //Set default arguments
57: $defaultArguments = [
58: 'verbose-logging' => false,
59: 'use-file-as-input' => false,
60: 'file-as-input-delay' => 10,
61: 'no-loop' => false,
62: 'command' => false,
63: 'json-read-cache-timeout' => 1,
64: 'json-url-read-cache-timeout' => 5,
65: 'check-syntax' => false,
66: 'sleep-on-error' => 0
67: ];
68:
69: $lineArguments = [];
70: //Check if there is an even number of arguments as the first argument is the filename
71: $argvCount = count($argv);
72: if($argvCount %2 === 0){
73: //Ignore arguments if even number of arguments (extra arguments number is arguments -1 as first argument is filename)
74: mklog(2, 'Missmach in arguments provided via commandline, ignoring last item');
75:
76: //Pretend odd item does not exist
77: $argvCount--;
78: }
79:
80: //Go through each provided command line argument
81: for($i = 1; $i < $argvCount; $i++){
82: //Pair up every other argument to the argument after it e.g. <arg1> <arg1value> <arg2> <arg2value>
83: $lineArguments[$argv[$i]] = $argv[$i+1];
84: $i++;
85: }
86: //Go through every command line argument
87: foreach($lineArguments as $lineArgumentName => $lineArgumentValue){
88: $lineArguments[$lineArgumentName] = data_types::convert_string($lineArgumentValue);
89: }
90:
91: //Now there are three argument arrays that will be overritten by the array after it: defaultArguments, fileArguments, lineArguments
92:
93: $arguments = [];
94: //Go through every known argument
95: foreach($defaultArguments as $defaultArgumentName => $defaultArgumentValue){
96: //Initially set argument value to default value
97: $arguments[$defaultArgumentName] = $defaultArgumentValue;
98: //Check if file argument for current argument is set
99: if(isset($fileArguments[$defaultArgumentName])){
100: //Overwrite argument value
101: $arguments[$defaultArgumentName] = $fileArguments[$defaultArgumentName];
102: }
103: //Check if command line argument for current argument is set
104: if(isset($lineArguments[$defaultArgumentName])){
105: //Overwrite argument value
106: $arguments[$defaultArgumentName] = $lineArguments[$defaultArgumentName];
107: }
108:
109: //Log stuff
110: if(verboseLogging()){
111: if(is_bool($arguments[$defaultArgumentName])){
112: $currentArgumentLogValue = $arguments[$defaultArgumentName] ? "true" : "false";
113: }
114: else{
115: $currentArgumentLogValue = $arguments[$defaultArgumentName];
116: }
117:
118: $type = gettype($arguments[$defaultArgumentName]);
119:
120: $colors = ["boolean"=>"light_blue", "integer"=>"light_green", "double"=>"light_green", "string"=>"light_red"];
121: $color = false;
122: if(isset($colors[$type])){
123: $color = $colors[$type];
124: }
125:
126: $defaultArgumentNameFormatted = cli_formatter::formatLine($defaultArgumentName, "white", false, false);
127: $currentArgumentLogValueFormatted = cli_formatter::formatLine($currentArgumentLogValue, $color, false, false);
128: $typeFormatted = cli_formatter::formatLine($type, "white", false, false);
129:
130: mklog(
131: 0,
132: 'Argument ' . $defaultArgumentName . ' is set to ' . $currentArgumentLogValue . ' with data type of ' . $type,
133: $color ? 'Argument ' . $defaultArgumentNameFormatted . ' is set to ' . $currentArgumentLogValueFormatted . ' with data type of ' . $typeFormatted : ''
134: );
135: }
136: }
137:
138: return $arguments;
139: })();
140:
141: unset($fileArguments);
142:
143: //////////
144:
145: echo "\033]0;PHP-CLI: " . getcwd() . "\007";
146:
147: require_once 'main.php';
148:
149: /**
150: * Makes a log entry and displays it to the user.
151: *
152: * @param integer $type Severity from 0 to 3 (inclusive) where 0 is for verbose messages, 1 is general, 2 is warning, and 3 is error.
153: * Error logs can pause the program for an amount of time in seconds if the sleep-on-error argument is above 0.
154: * @param string $message The message to be logged.
155: * @param string $formattedMessage Optionally a formatted version of the message that will be printed instead of the original message.
156: * The original message will still be saved.
157: * @return void If mklog has an issue it will shout at the user.
158: */
159: function mklog(int|string $type, string $message, string|bool $formattedMessage=''):void{
160: //Convert old to new format if detected
161: if(!is_int($type)){
162: mklog(0, 'The following log used an outdated version of mklog()');
163:
164: $type = substr(strtolower($type), 0, 1);
165:
166: if($type === "e"){$type = 3;}
167: elseif($type === "w"){$type = 2;}
168: else{$type = 1;}
169:
170: if($formattedMessage === true){
171: $type = 0;
172: }
173: }
174:
175: if(!is_string($formattedMessage)){
176: $formattedMessage = '';
177: }
178:
179: //
180:
181: $type = min(max($type,0),3);
182:
183: $verboseloggingsetting = verboseLogging();
184:
185: if($type || $verboseloggingsetting){
186:
187: $prefix = date("Y-m-d_H:i:s:") . substr(floor(microtime(true)*1000), -3) . ": " . ["Verbose","General","Warning","Error"][$type] . ": ";
188:
189: $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
190: if(isset($trace[1]['class'])){
191: $prefix .= $trace[1]['class'] . ': ';
192: }
193:
194: $cliFormatterExists = class_exists('cli_formatter');
195:
196: //Display log
197: $colour = "normal";
198: if($type && $verboseloggingsetting){
199: $colour = "light_green";
200: }
201: if($type === 2){$colour = "yellow";}
202: if($type === 3){$colour = "red";}
203:
204: if(!empty($formattedMessage)){
205: echo $prefix . $formattedMessage . "\n";
206: }
207: elseif($colour !== "normal" && $cliFormatterExists){
208: echo cli_formatter::formatLine($prefix . $message, $colour);
209: }
210: else{
211: echo $prefix . $message . "\n";
212: }
213:
214: //Write files
215: $stream = fopen('logs/latest.log','a');
216: if(!$stream){
217: echo "Error: Unable to open latest.log\n";
218: }
219: elseif(!fwrite($stream, $prefix . $message . "\n")){
220: echo "Error: Unable to write to latest.log\n";
221: }
222: elseif(!fclose($stream)){
223: echo "Error: Unable to save latest.log\n";
224: }
225:
226: $stream = fopen('logs/log-' . date("Y-m") . '.txt','a');
227: if(!$stream){
228: echo "Error: Unable to open logs file\n";
229: }
230: elseif(!fwrite($stream, $prefix . $message . "\n")){
231: echo "Error: Unable to write to logs file\n";
232: }
233: elseif(!fclose($stream)){
234: echo "Error: Unable to save logs file\n";
235: }
236: }
237:
238: if($type === 3){
239: if(isset($cliFormatterExists) && $cliFormatterExists){
240: cli_formatter::ding();
241: }
242: if($GLOBALS['arguments']['sleep-on-error'] > 0){
243: sleep($GLOBALS['arguments']['sleep-on-error']);
244: }
245: }
246: }
247: /**
248: * Gets weather verbose-logging is enabled.
249: *
250: * @return boolean Weather verbose-logging is enabled.
251: */
252: function verboseLogging():bool{
253: if(isset($GLOBALS['arguments']['verbose-logging'])){
254: if($GLOBALS['arguments']['verbose-logging']){
255: return true;
256: }
257: }
258: if(isset($GLOBALS['fileArguments']['verbose-logging'])){
259: if($GLOBALS['fileArguments']['verbose-logging']){
260: return true;
261: }
262: }
263: return false;
264: }
265: /**
266: * Returns a shell ready arguments string to preserve start arguments when restarting the program.
267: *
268: * @return string The shell escaped arguments.
269: */
270: function argsString():string{
271: global $argv;
272: return implode(' ', array_map('escapeshellarg', array_slice($argv, 1)));
273: }
274:
275: /**
276: * Gets info on the environment php-cli is running in. (ClaudeAI and ChatGPT helped, mostly works)
277: *
278: * @return array An array of booleans with string keys; windows, linux, desktop, headless, remote_desktop, ssh, modern_terminal, compat_layer, tty, interactive, daemon_or_cron.
279: */
280: function getEnvironment():array{
281: $isWindows = PHP_OS_FAMILY === 'Windows';
282: $isLinux = PHP_OS_FAMILY === 'Linux';
283:
284: // Unified environment accessor
285: $env = static fn(string $key): string|false => getenv($key);
286:
287: // --- Detect TTY / interactive console ---
288: $hasTty = false;
289: if(function_exists('stream_isatty')){
290: $hasTty = @stream_isatty(STDIN) || @stream_isatty(STDOUT);
291: }
292: elseif(function_exists('posix_isatty')){
293: $hasTty = @posix_isatty(STDIN) || @posix_isatty(STDOUT);
294: }
295:
296: // --- Shared checks ---
297: $isSSH = !empty($env('SSH_CLIENT')) || !empty($env('SSH_TTY'));
298:
299: // --- Defaults ---
300: $isDesktop = false;
301: $isRemoteDesktop = false;
302: $isModernTerminal = false;
303: $isCompatLayer = false;
304:
305: if($isWindows){
306: // CLIENTNAME is far more reliable than SESSIONNAME
307: // Local sessions normally report "Console"
308: // RDP sessions report remote client hostname
309: $clientName = $env('CLIENTNAME');
310: $isRemoteDesktop = !empty($clientName) && strtoupper($clientName) !== 'CONSOLE';
311:
312: // If we have a TTY and are not running through SSH,
313: // assume an interactive desktop session
314: $isDesktop = $hasTty && !$isSSH;
315:
316: // Modern terminal detection
317: $isModernTerminal =
318: !empty($env('WT_SESSION')) || // Windows Terminal
319: !empty($env('WT_PROFILE_ID')) ||
320: !empty($env('ConEmuPID')) || // ConEmu
321: !empty($env('TERM_PROGRAM')) || // VSCode/etc
322: !empty($env('ANSICON'));
323:
324: // Compatibility / subsystem layers
325: $isCompatLayer =
326: !empty($env('MSYSTEM')) || // MSYS2 / Git Bash
327: !empty($env('CYGWIN'));
328:
329: }
330: elseif($isLinux){
331:
332: $hasDisplay = !empty($env('DISPLAY')) || !empty($env('WAYLAND_DISPLAY'));
333:
334: $hasDesktopSession = !empty($env('DESKTOP_SESSION')) || !empty($env('XDG_CURRENT_DESKTOP')) || !empty($env('GNOME_DESKTOP_SESSION_ID')) || !empty($env('KDE_FULL_SESSION'));
335:
336: $isDesktop = $hasDisplay && $hasDesktopSession && !$isSSH;
337:
338: // Remote desktop detection
339: $isRemoteDesktop = !empty($env('XRDP_SESSION')) || !empty($env('X2GO_SESSION'));
340:
341: // Modern terminal detection
342: $term = $env('TERM');
343: $termProgram = $env('TERM_PROGRAM');
344:
345: $isModernTerminal = !empty($env('VTE_VERSION')) || !empty($termProgram) ||
346: in_array(
347: $term,
348: [
349: 'xterm-256color',
350: 'screen-256color',
351: 'tmux-256color',
352: 'alacritty',
353: 'wezterm',
354: ],
355: true
356: );
357:
358: // Wine / WSL / compatibility layers
359: $isCompatLayer = !empty($env('WINEPREFIX')) || file_exists('/proc/sys/fs/binfmt_misc/WSLInterop');
360:
361: }
362: else{
363: $isDesktop = $hasTty && !$isSSH;
364: }
365:
366: return [
367:
368: // Platform
369: 'windows' => $isWindows,
370: 'linux' => $isLinux,
371:
372: // Environment type
373: 'desktop' => $isDesktop,
374: 'headless' => !$isDesktop,
375: 'remote_desktop' => $isRemoteDesktop,
376: 'ssh' => $isSSH,
377: 'modern_terminal' => $isModernTerminal,
378: 'compat_layer' => $isCompatLayer,
379:
380: // TTY / interactivity
381: 'tty' => $hasTty,
382: 'interactive' => $hasTty && !$isSSH,
383: 'daemon_or_cron' => !$hasTty && !$isSSH,
384: ];
385: }