##// END OF EJS Templates
serve: add --cmdserver option to communicate with hg over a pipe
Idan Kamara -
r14647:2e9f379d default
parent child Browse files
Show More
@@ -0,0 +1,213 b''
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from i18n import _
9 import struct
10 import sys
11 import dispatch, encoding, util
12
13 logfile = None
14
15 def log(*args):
16 if not logfile:
17 return
18
19 for a in args:
20 logfile.write(str(a))
21
22 logfile.flush()
23
24 class channeledoutput(object):
25 """
26 Write data from in_ to out in the following format:
27
28 data length (unsigned int),
29 data
30 """
31 def __init__(self, in_, out, channel):
32 self.in_ = in_
33 self.out = out
34 self.channel = channel
35
36 def write(self, data):
37 if not data:
38 return
39 self.out.write(struct.pack('>cI', self.channel, len(data)))
40 self.out.write(data)
41 self.out.flush()
42
43 def __getattr__(self, attr):
44 if attr in ('isatty', 'fileno'):
45 raise AttributeError, attr
46 return getattr(self.in_, attr)
47
48 class channeledinput(object):
49 """
50 Read data from in_.
51
52 Requests for input are written to out in the following format:
53 channel identifier - 'I' for plain input, 'L' line based (1 byte)
54 how many bytes to send at most (unsigned int),
55
56 The client replies with:
57 data length (unsigned int), 0 meaning EOF
58 data
59 """
60
61 maxchunksize = 4 * 1024
62
63 def __init__(self, in_, out, channel):
64 self.in_ = in_
65 self.out = out
66 self.channel = channel
67
68 def read(self, size=-1):
69 if size < 0:
70 # if we need to consume all the clients input, ask for 4k chunks
71 # so the pipe doesn't fill up risking a deadlock
72 size = self.maxchunksize
73 s = self._read(size, self.channel)
74 buf = s
75 while s:
76 buf += s
77 s = self._read(size, self.channel)
78
79 return buf
80 else:
81 return self._read(size, self.channel)
82
83 def _read(self, size, channel):
84 if not size:
85 return ''
86 assert size > 0
87
88 # tell the client we need at most size bytes
89 self.out.write(struct.pack('>cI', channel, size))
90 self.out.flush()
91
92 length = self.in_.read(4)
93 length = struct.unpack('>I', length)[0]
94 if not length:
95 return ''
96 else:
97 return self.in_.read(length)
98
99 def readline(self, size=-1):
100 if size < 0:
101 size = self.maxchunksize
102 s = self._read(size, 'L')
103 buf = s
104 # keep asking for more until there's either no more or
105 # we got a full line
106 while s and s[-1] != '\n':
107 buf += s
108 s = self._read(size, 'L')
109
110 return buf
111 else:
112 return self._read(size, 'L')
113
114 def __iter__(self):
115 return self
116
117 def next(self):
118 l = self.readline()
119 if not l:
120 raise StopIteration
121 return l
122
123 def __getattr__(self, attr):
124 if attr in ('isatty', 'fileno'):
125 raise AttributeError, attr
126 return getattr(self.in_, attr)
127
128 class server(object):
129 """
130 Listens for commands on stdin, runs them and writes the output on a channel
131 based stream to stdout.
132 """
133 def __init__(self, ui, repo, mode):
134 self.ui = ui
135
136 logpath = ui.config("cmdserver", "log", None)
137 if logpath:
138 global logfile
139 if logpath == '-':
140 # write log on a special 'd'ebug channel
141 logfile = channeledoutput(sys.stdout, sys.stdout, 'd')
142 else:
143 logfile = open(logpath, 'a')
144
145 self.repo = repo
146
147 if mode == 'pipe':
148 self.cerr = channeledoutput(sys.stderr, sys.stdout, 'e')
149 self.cout = channeledoutput(sys.stdout, sys.stdout, 'o')
150 self.cin = channeledinput(sys.stdin, sys.stdout, 'I')
151 self.cresult = channeledoutput(sys.stdout, sys.stdout, 'r')
152
153 self.client = sys.stdin
154 else:
155 raise util.Abort(_('unknown mode %s') % mode)
156
157 def _read(self, size):
158 data = self.client.read(size)
159
160 # is the other end closed?
161 if not data:
162 raise EOFError()
163
164 return data
165
166 def runcommand(self):
167 """ reads a list of \0 terminated arguments, executes
168 and writes the return code to the result channel """
169
170 length = struct.unpack('>I', self._read(4))[0]
171 args = self._read(length).split('\0')
172
173 # copy the ui so changes to it don't persist between requests
174 req = dispatch.request(args, self.ui.copy(), self.repo, self.cin,
175 self.cout, self.cerr)
176
177 ret = dispatch.dispatch(req) or 0 # might return None
178
179 self.cresult.write(struct.pack('>i', int(ret)))
180
181 def getencoding(self):
182 """ writes the current encoding to the result channel """
183 self.cresult.write(encoding.encoding)
184
185 def serveone(self):
186 cmd = self.client.readline()[:-1]
187 if cmd:
188 handler = self.capabilities.get(cmd)
189 if handler:
190 handler(self)
191 else:
192 # clients are expected to check what commands are supported by
193 # looking at the servers capabilities
194 raise util.Abort(_('unknown command %s') % cmd)
195
196 return cmd != ''
197
198 capabilities = {'runcommand' : runcommand,
199 'getencoding' : getencoding}
200
201 def serve(self):
202 self.cout.write('capabilities: %s' % ' '.join(self.capabilities.keys()))
203 self.cout.write('encoding: %s' % encoding.encoding)
204
205 try:
206 while self.serveone():
207 pass
208 except EOFError:
209 # we'll get here if the client disconnected while we were reading
210 # its request
211 return 1
212
213 return 0
@@ -11,7 +11,8 b' from i18n import _, gettext'
11 import os, re, difflib, time, tempfile, errno
11 import os, re, difflib, time, tempfile, errno
12 import hg, scmutil, util, revlog, extensions, copies, error, bookmarks
12 import hg, scmutil, util, revlog, extensions, copies, error, bookmarks
13 import patch, help, url, encoding, templatekw, discovery
13 import patch, help, url, encoding, templatekw, discovery
14 import archival, changegroup, cmdutil, sshserver, hbisect, hgweb, hgweb.server
14 import archival, changegroup, cmdutil, hbisect
15 import sshserver, hgweb, hgweb.server, commandserver
15 import merge as mergemod
16 import merge as mergemod
16 import minirst, revset, fileset
17 import minirst, revset, fileset
17 import dagparser, context, simplemerge
18 import dagparser, context, simplemerge
@@ -4418,6 +4419,7 b' def root(ui, repo):'
4418 _('FILE')),
4419 _('FILE')),
4419 ('', 'pid-file', '', _('name of file to write process ID to'), _('FILE')),
4420 ('', 'pid-file', '', _('name of file to write process ID to'), _('FILE')),
4420 ('', 'stdio', None, _('for remote clients')),
4421 ('', 'stdio', None, _('for remote clients')),
4422 ('', 'cmdserver', '', _('for remote clients'), _('MODE')),
4421 ('t', 'templates', '', _('web templates to use'), _('TEMPLATE')),
4423 ('t', 'templates', '', _('web templates to use'), _('TEMPLATE')),
4422 ('', 'style', '', _('template style to use'), _('STYLE')),
4424 ('', 'style', '', _('template style to use'), _('STYLE')),
4423 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
4425 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
@@ -4448,13 +4450,24 b' def serve(ui, repo, **opts):'
4448 Returns 0 on success.
4450 Returns 0 on success.
4449 """
4451 """
4450
4452
4451 if opts["stdio"]:
4453 if opts["stdio"] and opts["cmdserver"]:
4454 raise util.Abort(_("cannot use --stdio with --cmdserver"))
4455
4456 def checkrepo():
4452 if repo is None:
4457 if repo is None:
4453 raise error.RepoError(_("There is no Mercurial repository here"
4458 raise error.RepoError(_("There is no Mercurial repository here"
4454 " (.hg not found)"))
4459 " (.hg not found)"))
4460
4461 if opts["stdio"]:
4462 checkrepo()
4455 s = sshserver.sshserver(ui, repo)
4463 s = sshserver.sshserver(ui, repo)
4456 s.serve_forever()
4464 s.serve_forever()
4457
4465
4466 if opts["cmdserver"]:
4467 checkrepo()
4468 s = commandserver.server(ui, repo, opts["cmdserver"])
4469 return s.serve()
4470
4458 # this way we can check if something was given in the command-line
4471 # this way we can check if something was given in the command-line
4459 if opts.get('port'):
4472 if opts.get('port'):
4460 opts['port'] = util.getport(opts.get('port'))
4473 opts['port'] = util.getport(opts.get('port'))
@@ -137,6 +137,7 b' Show the options for the "serve" command'
137 --accesslog
137 --accesslog
138 --address
138 --address
139 --certificate
139 --certificate
140 --cmdserver
140 --config
141 --config
141 --cwd
142 --cwd
142 --daemon
143 --daemon
@@ -199,7 +200,7 b' Show all commands + options'
199 pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
200 pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
200 push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
201 push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
201 remove: after, force, include, exclude
202 remove: after, force, include, exclude
202 serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, templates, style, ipv6, certificate
203 serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
203 status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
204 status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
204 summary: remote
205 summary: remote
205 update: clean, check, date, rev
206 update: clean, check, date, rev
@@ -79,7 +79,7 b' verify 7e7d56fe4833 (encoding fallback i'
79 >
79 >
80 > myui = ui.ui()
80 > myui = ui.ui()
81 > repo = hg.repository(myui, 'a')
81 > repo = hg.repository(myui, 'a')
82 > commands.serve(myui, repo, stdio=True)
82 > commands.serve(myui, repo, stdio=True, cmdserver=False)
83 > EOF
83 > EOF
84 $ echo baz >> b/foo
84 $ echo baz >> b/foo
85 $ hg -R b ci -m baz
85 $ hg -R b ci -m baz
General Comments 0
You need to be logged in to leave comments. Login now