Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / diffusion / ssh / DiffusionGitSSHWorkflow.php
blob292741e34d90d61998d0904d2d71d4457933fce4
1 <?php
3 abstract class DiffusionGitSSHWorkflow
4 extends DiffusionSSHWorkflow
5 implements DiffusionRepositoryClusterEngineLogInterface {
7 private $engineLogProperties = array();
8 private $protocolLog;
10 private $wireProtocol;
11 private $ioBytesRead = 0;
12 private $ioBytesWritten = 0;
13 private $requestAttempts = 0;
14 private $requestFailures = 0;
16 protected function writeError($message) {
17 // Git assumes we'll add our own newlines.
18 return parent::writeError($message."\n");
21 public function writeClusterEngineLogMessage($message) {
22 parent::writeError($message);
23 $this->getErrorChannel()->update();
26 public function writeClusterEngineLogProperty($key, $value) {
27 $this->engineLogProperties[$key] = $value;
30 protected function getClusterEngineLogProperty($key, $default = null) {
31 return idx($this->engineLogProperties, $key, $default);
34 protected function identifyRepository() {
35 $args = $this->getArgs();
36 $path = head($args->getArg('dir'));
37 return $this->loadRepositoryWithPath(
38 $path,
39 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
42 protected function waitForGitClient() {
43 $io_channel = $this->getIOChannel();
45 // If we don't wait for the client to close the connection, `git` will
46 // consider it an early abort and fail. Sit around until Git is comfortable
47 // that it really received all the data.
48 while ($io_channel->isOpenForReading()) {
49 $io_channel->update();
50 $this->getErrorChannel()->flush();
51 PhutilChannel::waitForAny(array($io_channel));
55 protected function raiseWrongVCSException(
56 PhabricatorRepository $repository) {
57 throw new Exception(
58 pht(
59 'This repository ("%s") is not a Git repository. Use "%s" to '.
60 'interact with this repository.',
61 $repository->getDisplayName(),
62 $repository->getVersionControlSystem()));
65 protected function newPassthruCommand() {
66 return parent::newPassthruCommand()
67 ->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
68 ->setWillReadCallback(array($this, 'willReadMessageCallback'));
71 protected function newProtocolLog($is_proxy) {
72 if ($is_proxy) {
73 return null;
76 // While developing, do this to write a full protocol log to disk:
78 // return new PhabricatorProtocolLog('/tmp/git-protocol.log');
80 return null;
83 final protected function getProtocolLog() {
84 return $this->protocolLog;
87 final protected function setProtocolLog(PhabricatorProtocolLog $log) {
88 $this->protocolLog = $log;
91 final protected function getWireProtocol() {
92 return $this->wireProtocol;
95 final protected function setWireProtocol(
96 DiffusionGitWireProtocol $protocol) {
97 $this->wireProtocol = $protocol;
98 return $this;
101 public function willWriteMessageCallback(
102 PhabricatorSSHPassthruCommand $command,
103 $message) {
105 $this->ioBytesWritten += strlen($message);
107 $log = $this->getProtocolLog();
108 if ($log) {
109 $log->didWriteBytes($message);
112 $protocol = $this->getWireProtocol();
113 if ($protocol) {
114 $message = $protocol->willWriteBytes($message);
117 return $message;
120 public function willReadMessageCallback(
121 PhabricatorSSHPassthruCommand $command,
122 $message) {
124 $log = $this->getProtocolLog();
125 if ($log) {
126 $log->didReadBytes($message);
129 $protocol = $this->getWireProtocol();
130 if ($protocol) {
131 $message = $protocol->willReadBytes($message);
134 // Note that bytes aren't counted until they're emittted by the protocol
135 // layer. This means the underlying command might emit bytes, but if they
136 // are buffered by the protocol layer they won't count as read bytes yet.
138 $this->ioBytesRead += strlen($message);
140 return $message;
143 final protected function getIOBytesRead() {
144 return $this->ioBytesRead;
147 final protected function getIOBytesWritten() {
148 return $this->ioBytesWritten;
151 final protected function executeRepositoryProxyOperations($for_write) {
152 $device = AlmanacKeys::getLiveDevice();
154 $refs = $this->getAlmanacServiceRefs($for_write);
155 $err = 1;
157 while (true) {
158 $ref = head($refs);
160 $command = $this->getProxyCommandForServiceRef($ref);
162 if ($device) {
163 $this->writeClusterEngineLogMessage(
164 pht(
165 "# Request received by \"%s\", forwarding to cluster ".
166 "host \"%s\".\n",
167 $device->getName(),
168 $ref->getDeviceName()));
171 $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
173 $future = id(new ExecFuture('%C', $command))
174 ->setEnv($this->getEnvironment());
176 $this->didBeginRequest();
178 $err = $this->newPassthruCommand()
179 ->setIOChannel($this->getIOChannel())
180 ->setCommandChannelFromExecFuture($future)
181 ->execute();
183 // TODO: Currently, when proxying, we do not write an event log on the
184 // proxy. Perhaps we should write a "proxy log". This is not very useful
185 // for statistics or auditing, but could be useful for diagnostics.
186 // Marking the proxy logs as proxied (and recording devicePHID on all
187 // logs) would make differentiating between these use cases easier.
189 if (!$err) {
190 $this->waitForGitClient();
191 return $err;
194 // Throw away this service: the request failed and we're treating the
195 // failure as persistent, so we don't want to retry another request to
196 // the same host.
197 array_shift($refs);
199 $should_retry = $this->shouldRetryRequest($refs);
200 if (!$should_retry) {
201 return $err;
204 // If we haven't bailed out yet, we'll retry the request with the next
205 // service.
208 throw new Exception(pht('Reached an unreachable place.'));
211 private function didBeginRequest() {
212 $this->requestAttempts++;
213 return $this;
216 private function shouldRetryRequest(array $remaining_refs) {
217 $this->requestFailures++;
219 if ($this->requestFailures > $this->requestAttempts) {
220 throw new Exception(
221 pht(
222 "Workflow has recorded more failures than attempts; there is a ".
223 "missing call to \"didBeginRequest()\".\n"));
226 if (!$remaining_refs) {
227 $this->writeClusterEngineLogMessage(
228 pht(
229 "# All available services failed to serve the request, ".
230 "giving up.\n"));
231 return false;
234 $read_len = $this->getIOBytesRead();
235 if ($read_len) {
236 $this->writeClusterEngineLogMessage(
237 pht(
238 "# Client already read from service (%s bytes), unable to retry.\n",
239 new PhutilNumber($read_len)));
240 return false;
243 $write_len = $this->getIOBytesWritten();
244 if ($write_len) {
245 $this->writeClusterEngineLogMessage(
246 pht(
247 "# Client already wrote to service (%s bytes), unable to retry.\n",
248 new PhutilNumber($write_len)));
249 return false;
252 $this->writeClusterEngineLogMessage(
253 pht(
254 "# Service request failed, retrying (making attempt %s of %s).\n",
255 new PhutilNumber($this->requestAttempts + 1),
256 new PhutilNumber($this->requestAttempts + count($remaining_refs))));
258 return true;