4
* Channel to a Mercurial "cmdserver" client. For a detailed description of the
5
* "cmdserver" protocol, see @{class:ArcanistHgServerChannel}. This channel
6
* implements the other half of the protocol: it decodes messages from the
7
* client and encodes messages from the server.
9
* Because the proxy server speaks the exact same protocol that Mercurial
10
* does and fully decodes both sides of the protocol, we need this half of the
11
* decode/encode to talk to clients. Without it, we wouldn't be able to
12
* determine when a client request had completed and was ready for transmission
13
* to the Mercurial server.
15
* (Technically, we could get away without re-encoding messages from the
16
* server, but the serialization is not complicated and having a general
17
* implementation of encoded/decode for both the client and server dialects
20
* @task protocol Protocol Implementation
22
final class ArcanistHgClientChannel extends PhutilProtocolChannel {
24
const MODE_COMMAND = 'command';
25
const MODE_LENGTH = 'length';
26
const MODE_ARGUMENTS = 'arguments';
29
private $byteLengthOfNextChunk;
32
private $mode = self::MODE_COMMAND;
35
/* -( Protocol Implementation )-------------------------------------------- */
39
* Encode a message for transmission to the client. The message should be
40
* a pair with the channel name and the a block of data, like this:
42
* array('o', '<some data...>');
44
* We encode it like this:
47
* 1234 # Length, as a 4-byte unsigned long.
50
* For a detailed description of the cmdserver protocol, see
51
* @{class:ArcanistHgServerChannel}.
53
* @param pair<string,string> The <channel, data> pair to encode.
54
* @return string Encoded string for transmission to the client.
58
protected function encodeMessage($argv) {
59
if (!is_array($argv) || count($argv) !== 2) {
60
throw new Exception('Message should be <channel, data>.');
63
$channel = head($argv);
67
$len = pack('N', $len);
69
return "{$channel}{$len}{$data}";
74
* Decode a message received from the client. The message looks like this:
77
* 8 # Length, as a 4-byte unsigned long.
82
* We decode it into a list in PHP, which looks like this:
91
* @param string Bytes from the server.
92
* @return list<list<string>> Zero or more complete commands.
96
protected function decodeStream($data) {
99
// The first part is terminated by "\n", so we don't always know how many
100
// bytes we need to look for. This makes parsing a bit of a pain.
105
$continue_parsing = false;
107
switch ($this->mode) {
108
case self::MODE_COMMAND:
109
// We're looking for "\n", which indicates the end of the command
110
// name, like "runcommand". Next, we'll expect a length.
112
$pos = strpos($this->buf, "\n");
113
if ($pos === false) {
117
$this->command = substr($this->buf, 0, $pos);
118
$this->buf = substr($this->buf, $pos + 1);
119
$this->mode = self::MODE_LENGTH;
121
$continue_parsing = true;
123
case self::MODE_LENGTH:
124
// We're looking for a byte length, as a 4-byte big-endian unsigned
125
// integer. Next, we'll expect that many bytes of data.
127
if (strlen($this->buf) < 4) {
131
$len = substr($this->buf, 0, 4);
132
$len = unpack('N', $len);
135
$this->buf = substr($this->buf, 4);
137
$this->mode = self::MODE_ARGUMENTS;
138
$this->byteLengthOfNextChunk = $len;
140
$continue_parsing = true;
142
case self::MODE_ARGUMENTS:
143
// We're looking for the data itself, which is a block of bytes
144
// of the given length. These are arguments delimited by "\0". Next
145
// we'll expect another command.
147
if (strlen($this->buf) < $this->byteLengthOfNextChunk) {
151
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
152
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
154
$message = array_merge(array($this->command), explode("\0", $data));
156
$this->mode = self::MODE_COMMAND;
157
$this->command = null;
158
$this->byteLengthOfNextChunk = null;
160
$messages[] = $message;
162
$continue_parsing = true;
165
} while ($continue_parsing);