@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3abstract class PhabricatorSetupCheck extends Phobject {
4
5 private $issues;
6
7 abstract protected function executeChecks();
8
9 const GROUP_OTHER = 'other';
10 const GROUP_MYSQL = 'mysql';
11 const GROUP_PHP = 'php';
12 const GROUP_IMPORTANT = 'important';
13
14 public function getExecutionOrder() {
15 if ($this->isPreflightCheck()) {
16 return 0;
17 } else {
18 return 1000;
19 }
20 }
21
22 /**
23 * Should this check execute before we load configuration?
24 *
25 * The majority of checks (particularly, those checks which examine
26 * configuration) should run in the normal setup phase, after configuration
27 * loads. However, a small set of critical checks (mostly, tests for PHP
28 * setup and extensions) need to run before we can load configuration.
29 *
30 * @return bool True to execute before configuration is loaded.
31 */
32 public function isPreflightCheck() {
33 return false;
34 }
35
36 final protected function newIssue($key) {
37 $issue = id(new PhabricatorSetupIssue())
38 ->setIssueKey($key);
39 $this->issues[$key] = $issue;
40
41 if ($this->getDefaultGroup()) {
42 $issue->setGroup($this->getDefaultGroup());
43 }
44
45 return $issue;
46 }
47
48 final public function getIssues() {
49 return $this->issues;
50 }
51
52 protected function addIssue(PhabricatorSetupIssue $issue) {
53 $this->issues[$issue->getIssueKey()] = $issue;
54 return $this;
55 }
56
57 public function getDefaultGroup() {
58 return null;
59 }
60
61 final public function runSetupChecks() {
62 $this->issues = array();
63 $this->executeChecks();
64 }
65
66 final public static function getOpenSetupIssueKeys() {
67 $cache = PhabricatorCaches::getSetupCache();
68 return $cache->getKey('phabricator.setup.issue-keys');
69 }
70
71 final public static function resetSetupState() {
72 $cache = PhabricatorCaches::getSetupCache();
73 $cache->deleteKey('phabricator.setup.issue-keys');
74
75 $server_cache = PhabricatorCaches::getServerStateCache();
76 $server_cache->deleteKey('phabricator.in-flight');
77
78 $use_scope = AphrontWriteGuard::isGuardActive();
79 if ($use_scope) {
80 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
81 } else {
82 AphrontWriteGuard::allowDangerousUnguardedWrites(true);
83 }
84
85 try {
86 $db_cache = new PhabricatorKeyValueDatabaseCache();
87 $db_cache->deleteKey('phabricator.setup.issue-keys');
88 } catch (Exception $ex) {
89 // If we hit an exception here, just ignore it. In particular, this can
90 // happen on initial startup before the databases are initialized.
91 }
92
93 if ($use_scope) {
94 unset($unguarded);
95 } else {
96 AphrontWriteGuard::allowDangerousUnguardedWrites(false);
97 }
98 }
99
100 final public static function setOpenSetupIssueKeys(
101 array $keys,
102 $update_database) {
103 $cache = PhabricatorCaches::getSetupCache();
104 $cache->setKey('phabricator.setup.issue-keys', $keys);
105
106 $server_cache = PhabricatorCaches::getServerStateCache();
107 $server_cache->setKey('phabricator.in-flight', 1);
108
109 if ($update_database) {
110 $db_cache = new PhabricatorKeyValueDatabaseCache();
111 try {
112 $json = phutil_json_encode($keys);
113 $db_cache->setKey('phabricator.setup.issue-keys', $json);
114 } catch (Exception $ex) {
115 // Ignore any write failures, since they likely just indicate that we
116 // have a database-related setup issue that needs to be resolved.
117 }
118 }
119 }
120
121 final public static function getOpenSetupIssueKeysFromDatabase() {
122 $db_cache = new PhabricatorKeyValueDatabaseCache();
123 try {
124 $value = $db_cache->getKey('phabricator.setup.issue-keys');
125 if (!phutil_nonempty_string($value)) {
126 return null;
127 }
128 return phutil_json_decode($value);
129 } catch (Exception $ex) {
130 return null;
131 }
132 }
133
134 /**
135 * @param array<PhabricatorSetupIssue> $all_issues
136 */
137 final public static function getUnignoredIssueKeys(array $all_issues) {
138 assert_instances_of($all_issues, PhabricatorSetupIssue::class);
139 $keys = array();
140 foreach ($all_issues as $issue) {
141 if (!$issue->getIsIgnored()) {
142 $keys[] = $issue->getIssueKey();
143 }
144 }
145 return $keys;
146 }
147
148 final public static function getConfigNeedsRepair() {
149 $cache = PhabricatorCaches::getSetupCache();
150 return $cache->getKey('phabricator.setup.needs-repair');
151 }
152
153 final public static function setConfigNeedsRepair($needs_repair) {
154 $cache = PhabricatorCaches::getSetupCache();
155 $cache->setKey('phabricator.setup.needs-repair', $needs_repair);
156 }
157
158 final public static function deleteSetupCheckCache() {
159 $cache = PhabricatorCaches::getSetupCache();
160 $cache->deleteKeys(
161 array(
162 'phabricator.setup.needs-repair',
163 'phabricator.setup.issue-keys',
164 ));
165 }
166
167 final public static function willPreflightRequest() {
168 $checks = self::loadAllChecks();
169
170 foreach ($checks as $check) {
171 if (!$check->isPreflightCheck()) {
172 continue;
173 }
174
175 $check->runSetupChecks();
176
177 foreach ($check->getIssues() as $key => $issue) {
178 return self::newIssueResponse($issue);
179 }
180 }
181
182 return null;
183 }
184
185 public static function newIssueResponse(PhabricatorSetupIssue $issue) {
186 $view = id(new PhabricatorSetupIssueView())
187 ->setIssue($issue);
188
189 return id(new PhabricatorConfigResponse())
190 ->setView($view);
191 }
192
193 final public static function willProcessRequest() {
194 $issue_keys = self::getOpenSetupIssueKeys();
195 if ($issue_keys === null) {
196 $engine = new PhabricatorSetupEngine();
197 $response = $engine->execute();
198 if ($response) {
199 return $response;
200 }
201 } else if ($issue_keys) {
202 // If Phabricator is configured in a cluster with multiple web devices,
203 // we can end up with setup issues cached on every device. This can cause
204 // a warning banner to show on every device so that each one needs to
205 // be dismissed individually, which is pretty annoying. See T10876.
206
207 // To avoid this, check if the issues we found have already been cleared
208 // in the database. If they have, we'll just wipe out our own cache and
209 // move on.
210 $issue_keys = self::getOpenSetupIssueKeysFromDatabase();
211 if ($issue_keys !== null) {
212 self::setOpenSetupIssueKeys($issue_keys, $update_database = false);
213 }
214 }
215
216 // Try to repair configuration unless we have a clean bill of health on it.
217 // We need to keep doing this on every page load until all the problems
218 // are fixed, which is why it's separate from setup checks (which run
219 // once per restart).
220 $needs_repair = self::getConfigNeedsRepair();
221 if ($needs_repair !== false) {
222 $needs_repair = self::repairConfig();
223 self::setConfigNeedsRepair($needs_repair);
224 }
225 }
226
227 /**
228 * Test if we've survived through setup on at least one normal request
229 * without fataling.
230 *
231 * If we've made it through setup without hitting any fatals, we switch
232 * to render a more friendly error page when encountering issues like
233 * database connection failures. This gives users a smoother experience in
234 * the face of intermittent failures.
235 *
236 * @return bool True if we've made it through setup since the last restart.
237 */
238 final public static function isInFlight() {
239 $cache = PhabricatorCaches::getServerStateCache();
240 return (bool)$cache->getKey('phabricator.in-flight');
241 }
242
243 final public static function loadAllChecks() {
244 return id(new PhutilClassMapQuery())
245 ->setAncestorClass(self::class)
246 ->setSortMethod('getExecutionOrder')
247 ->execute();
248 }
249
250 final public static function runNormalChecks() {
251 $checks = self::loadAllChecks();
252
253 foreach ($checks as $key => $check) {
254 if ($check->isPreflightCheck()) {
255 unset($checks[$key]);
256 }
257 }
258
259 $issues = array();
260 foreach ($checks as $check) {
261 $check->runSetupChecks();
262 foreach ($check->getIssues() as $key => $issue) {
263 if (isset($issues[$key])) {
264 throw new Exception(
265 pht(
266 "Two setup checks raised an issue with key '%s'!",
267 $key));
268 }
269 $issues[$key] = $issue;
270 if ($issue->getIsFatal()) {
271 break 2;
272 }
273 }
274 }
275
276 $ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
277 foreach ($ignore_issues as $ignorable => $derp) {
278 if (isset($issues[$ignorable])) {
279 $issues[$ignorable]->setIsIgnored(true);
280 }
281 }
282
283 return $issues;
284 }
285
286 final public static function repairConfig() {
287 $needs_repair = false;
288
289 $options = PhabricatorApplicationConfigOptions::loadAllOptions();
290 foreach ($options as $option) {
291 try {
292 $option->getGroup()->validateOption(
293 $option,
294 PhabricatorEnv::getEnvConfig($option->getKey()));
295 } catch (PhabricatorConfigValidationException $ex) {
296 PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
297 $needs_repair = true;
298 }
299 }
300
301 return $needs_repair;
302 }
303
304}