PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / DiagnosticDirectoryProbe.php

DiagnosticDirectoryProbe.php in 404 Solution trunk, at includes/diagnostics/DiagnosticDirectoryProbe.php

114 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Proof that the diagnostics directory is genuinely usable for THIS request.
9 *
10 * A resolved directory path is not evidence of anything. Beta.1 ended with
11 * journals that "came back empty", and an empty journal has several
12 * indistinguishable causes: a path that resolved but is not writable, a host
13 * that writes to a per-worker filesystem, an open_basedir restriction, a quota,
14 * a directory that exists for the writing worker and not for the reading one.
15 * Only an actual round trip can separate those, and only if it is performed by
16 * the request being diagnosed rather than by a health check at some other time.
17 *
18 * So this walks the whole file lifecycle -- create, append, flush, stat, glob,
19 * read back, delete -- against a request-specific file in that directory, and
20 * journals the outcome of every step. The step name in a failed record IS the
21 * finding: "stat" says the bytes did not land, "glob" says the directory cannot
22 * be enumerated (which is exactly what the support collector later has to do),
23 * "delete" says the plugin is accumulating files it cannot clean up.
24 *
25 * The results go through ABJ_404_Solution_AjaxCheckpointLogger, the immediate
26 * append-only channel, rather than through the staged trace journal this is
27 * partly probing on behalf of: a defect in the component under investigation
28 * must not be able to erase the evidence about it. That makes this class a
29 * PRODUCER of checkpoint records, like every other instrumented boundary, and
30 * the recorder knows nothing about it.
31 */
32 final class ABJ_404_Solution_DiagnosticDirectoryProbe {
33
34 /** Prefix for the probe file, also the glob the enumeration step tests. */
35 const FILE_PREFIX = 'abj404_checkpoint_selftest_';
36
37 /**
38 * Run the whole round trip and journal each step's outcome. Never throws:
39 * a probe that cannot complete is a finding about the host, and it must not
40 * become the reason the request it was proving fails.
41 */
42 public static function run(string $requestId): void {
43 try {
44 $directory = ABJ_404_Solution_AjaxCheckpointLogger::resolveDirectory();
45 if ($directory === '') {
46 self::recordStep($requestId, false, 'resolve_directory');
47 return;
48 }
49 $path = $directory . self::FILE_PREFIX . $requestId . '_'
50 . ABJ_404_Solution_PhpRuntimeCapabilityAdapter::processToken() . '.tmp';
51 $payload = 'abj404-selftest-' . $requestId;
52
53 $handle = @fopen($path, 'wb');
54 if ($handle === false) {
55 self::recordStep($requestId, false, 'create');
56 return;
57 }
58 $written = @fwrite($handle, $payload);
59 $flushed = @fflush($handle);
60 @fclose($handle);
61 if ($written === false || !$flushed) {
62 self::recordStep($requestId, false, 'append_flush');
63 return;
64 }
65
66 $size = @filesize($path);
67 if (!is_int($size) || $size !== strlen($payload)) {
68 self::recordStep($requestId, false, 'stat', array('size' => $size));
69 return;
70 }
71
72 $globMatches = @glob($directory . self::FILE_PREFIX . '*');
73 $globCount = is_array($globMatches) ? count($globMatches) : 0;
74 if ($globCount < 1) {
75 self::recordStep($requestId, false, 'glob', array('glob_count' => $globCount));
76 return;
77 }
78
79 $readBack = @file_get_contents($path);
80 if ($readBack !== $payload) {
81 self::recordStep($requestId, false, 'read');
82 return;
83 }
84
85 if (!@unlink($path)) {
86 self::recordStep($requestId, false, 'delete');
87 return;
88 }
89
90 self::recordStep($requestId, true, 'complete', array('glob_count' => $globCount));
91 } catch (Throwable $e) {
92 // Reported to the debug log as well as journaled: an exception here
93 // is a host condition the plugin cannot act on, and the journal is
94 // read only when an admin sends a support request.
95 abj404_logPhpFallback('ajax-checkpoint',
96 'AJAX checkpoint self-test failed: ' . $e->getMessage());
97 self::recordStep($requestId, false, 'exception',
98 array('message' => substr($e->getMessage(), 0, 200)));
99 }
100 }
101
102 /**
103 * One step outcome, under the event name the evidence catalogue and both
104 * acceptance gates already know this probe by.
105 *
106 * @param array<string, mixed> $extra
107 */
108 private static function recordStep(string $requestId, bool $ok, string $step,
109 array $extra = array()): void {
110 ABJ_404_Solution_AjaxCheckpointLogger::record($requestId, 'selftest',
111 array_merge(array('ok' => $ok, 'step' => $step), $extra));
112 }
113 }
114