-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground-runner.php
More file actions
155 lines (132 loc) · 4.83 KB
/
Copy pathbackground-runner.php
File metadata and controls
155 lines (132 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<?php
/***************
* Background Task Runner
*
* Internal script — invoked exclusively by BackgroundTaskDispatcher::dispatch().
* Do not call this script directly.
*
* Usage:
* php background-runner.php /path/to/bgtask_<id>.json
*
* The payload file is a JSON object:
* { "class": "Fully\\Qualified\\TaskClass", "args": { ... } }
*
* The file is deleted immediately after being read.
***************/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit(1);
}
if ($argc !== 2) {
fwrite(STDERR, "Usage: background-runner.php <task-payload-file>\n");
exit(1);
}
$payloadFile = $argv[1];
if (!is_file($payloadFile)) {
fwrite(STDERR, "Payload file not found: $payloadFile\n");
exit(1);
}
// ─── Read and immediately delete the payload file ───
$raw = file_get_contents($payloadFile);
unlink($payloadFile);
if ($raw === false) {
fwrite(STDERR, "Could not read payload file.\n");
exit(1);
}
// ─── Decode payload ───
try {
$payload = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
fwrite(STDERR, "Invalid payload JSON: " . $e->getMessage() . "\n");
exit(1);
}
$class = $payload['class'] ?? '';
$file = $payload['file'] ?? null;
$args = $payload['args'] ?? [];
$statusFile = $payload['statusFile'] ?? null;
$context = $payload['context'] ?? null;
$appRoot = $payload['appRoot'] ?? null;
if (!is_string($class) || $class === '') {
fwrite(STDERR, "Payload missing 'class'.\n");
exit(1);
}
// ─── Validate and normalise the status file path ───
if ($statusFile !== null) {
if (!preg_match('#/bgtask_status_[0-9a-f]{32}\.json$#', $statusFile)) {
fwrite(STDERR, "Invalid status file path.\n");
$statusFile = null; // don't write status, but still run the task
}
}
/** Atomically write a JSON status blob. */
$writeStatus = static function (array $data) use ($statusFile): void {
if ($statusFile !== null) {
file_put_contents($statusFile, json_encode($data), LOCK_EX);
}
};
$writeStatus(['status' => 'running']);
try {
// ─── Load application context ───
if ($context !== null) {
if (!is_file($context)) {
throw new \RuntimeException("Context file not found: $context");
}
// Use the explicitly set appRoot from the payload (set via setAppRoot() in
// the web entry point using __DIR__, which is never symlinked there).
// Fall back to deriving from the context path if not set.
$backgroundAppRoot = $appRoot ?? rtrim(dirname($context, 2), '/') . '/';
require $context;
} else {
fwrite(STDERR, "WARNING: no context file set — application classes will not be autoloaded\n");
}
// ─── Optionally require the task file (for non-autoloaded classes) ───
if ($file !== null) {
// Security: only allow files within the application root or the shared
// symlink-target root (module/ etc. are symlinks whose realpath resolves
// to a different directory than the per-instance $appRoot).
$instanceRoot = realpath($backgroundAppRoot ?? dirname($context ?? '', 2));
$realFile = realpath($file);
// Derive the shared root by resolving the symlink on module/ inside the
// instance root — gives us e.g. /srv/a/kokonotsuba from
// /srv/a/kokobackend1/module -> /srv/a/kokonotsuba/module.
$sharedRoot = ($instanceRoot !== false && is_dir($instanceRoot . '/module'))
? realpath($instanceRoot . '/module')
: false;
$sharedRoot = ($sharedRoot !== false) ? dirname($sharedRoot) : false;
$inInstance = $instanceRoot !== false && $realFile !== false
&& strncmp($realFile, $instanceRoot, strlen($instanceRoot)) === 0;
$inShared = $sharedRoot !== false && $realFile !== false
&& strncmp($realFile, $sharedRoot, strlen($sharedRoot)) === 0;
if ($realFile === false || (!$inInstance && !$inShared)) {
throw new \RuntimeException("Task file '$file' is outside the application directory or does not exist.");
}
require_once $realFile;
}
// ─── Validate task class ───
if (!class_exists($class)) {
$writeStatus(['status' => 'failed', 'error' => "Task class not found: $class"]);
fwrite(STDERR, "Task class not found: $class\n");
exit(1);
}
if (!is_a($class, \Puchiko\background\BackgroundTaskInterface::class, true)) {
throw new \RuntimeException("Class $class does not implement BackgroundTaskInterface.");
}
// ─── Execute task ───
/** @var \Puchiko\background\BackgroundTaskInterface $task */
$task = new $class();
$task->handle((array) $args);
$writeStatus(['status' => 'completed']);
} catch (\Throwable $e) {
$errMsg = sprintf(
"Task %s failed: [%s] %s in %s:%d\nStack trace:\n%s\n",
$class,
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
fwrite(STDERR, $errMsg);
$writeStatus(['status' => 'failed', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
exit(1);
}
exit(0);