##// END OF EJS Templates
commandserver: turn server debug messages into logs...
Yuya Nishihara -
r40863:25e9089c default
parent child Browse files
Show More
@@ -1,636 +1,637
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 """command server extension for cHg
8 """command server extension for cHg
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'setenv' command
19 'setenv' command
20 replace os.environ completely
20 replace os.environ completely
21
21
22 'setumask' command (DEPRECATED)
22 'setumask' command (DEPRECATED)
23 'setumask2' command
23 'setumask2' command
24 set umask
24 set umask
25
25
26 'validate' command
26 'validate' command
27 reload the config and check if the server is up to date
27 reload the config and check if the server is up to date
28
28
29 Config
29 Config
30 ------
30 ------
31
31
32 ::
32 ::
33
33
34 [chgserver]
34 [chgserver]
35 # how long (in seconds) should an idle chg server exit
35 # how long (in seconds) should an idle chg server exit
36 idletimeout = 3600
36 idletimeout = 3600
37
37
38 # whether to skip config or env change checks
38 # whether to skip config or env change checks
39 skiphash = False
39 skiphash = False
40 """
40 """
41
41
42 from __future__ import absolute_import
42 from __future__ import absolute_import
43
43
44 import hashlib
44 import hashlib
45 import inspect
45 import inspect
46 import os
46 import os
47 import re
47 import re
48 import socket
48 import socket
49 import stat
49 import stat
50 import struct
50 import struct
51 import time
51 import time
52
52
53 from .i18n import _
53 from .i18n import _
54
54
55 from . import (
55 from . import (
56 commandserver,
56 commandserver,
57 encoding,
57 encoding,
58 error,
58 error,
59 extensions,
59 extensions,
60 node,
60 node,
61 pycompat,
61 pycompat,
62 util,
62 util,
63 )
63 )
64
64
65 from .utils import (
65 from .utils import (
66 procutil,
66 procutil,
67 )
67 )
68
68
69 def _hashlist(items):
69 def _hashlist(items):
70 """return sha1 hexdigest for a list"""
70 """return sha1 hexdigest for a list"""
71 return node.hex(hashlib.sha1(str(items)).digest())
71 return node.hex(hashlib.sha1(str(items)).digest())
72
72
73 # sensitive config sections affecting confighash
73 # sensitive config sections affecting confighash
74 _configsections = [
74 _configsections = [
75 'alias', # affects global state commands.table
75 'alias', # affects global state commands.table
76 'eol', # uses setconfig('eol', ...)
76 'eol', # uses setconfig('eol', ...)
77 'extdiff', # uisetup will register new commands
77 'extdiff', # uisetup will register new commands
78 'extensions',
78 'extensions',
79 ]
79 ]
80
80
81 _configsectionitems = [
81 _configsectionitems = [
82 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
82 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
83 ]
83 ]
84
84
85 # sensitive environment variables affecting confighash
85 # sensitive environment variables affecting confighash
86 _envre = re.compile(r'''\A(?:
86 _envre = re.compile(r'''\A(?:
87 CHGHG
87 CHGHG
88 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
88 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
89 |HG(?:ENCODING|PLAIN).*
89 |HG(?:ENCODING|PLAIN).*
90 |LANG(?:UAGE)?
90 |LANG(?:UAGE)?
91 |LC_.*
91 |LC_.*
92 |LD_.*
92 |LD_.*
93 |PATH
93 |PATH
94 |PYTHON.*
94 |PYTHON.*
95 |TERM(?:INFO)?
95 |TERM(?:INFO)?
96 |TZ
96 |TZ
97 )\Z''', re.X)
97 )\Z''', re.X)
98
98
99 def _confighash(ui):
99 def _confighash(ui):
100 """return a quick hash for detecting config/env changes
100 """return a quick hash for detecting config/env changes
101
101
102 confighash is the hash of sensitive config items and environment variables.
102 confighash is the hash of sensitive config items and environment variables.
103
103
104 for chgserver, it is designed that once confighash changes, the server is
104 for chgserver, it is designed that once confighash changes, the server is
105 not qualified to serve its client and should redirect the client to a new
105 not qualified to serve its client and should redirect the client to a new
106 server. different from mtimehash, confighash change will not mark the
106 server. different from mtimehash, confighash change will not mark the
107 server outdated and exit since the user can have different configs at the
107 server outdated and exit since the user can have different configs at the
108 same time.
108 same time.
109 """
109 """
110 sectionitems = []
110 sectionitems = []
111 for section in _configsections:
111 for section in _configsections:
112 sectionitems.append(ui.configitems(section))
112 sectionitems.append(ui.configitems(section))
113 for section, item in _configsectionitems:
113 for section, item in _configsectionitems:
114 sectionitems.append(ui.config(section, item))
114 sectionitems.append(ui.config(section, item))
115 sectionhash = _hashlist(sectionitems)
115 sectionhash = _hashlist(sectionitems)
116 # If $CHGHG is set, the change to $HG should not trigger a new chg server
116 # If $CHGHG is set, the change to $HG should not trigger a new chg server
117 if 'CHGHG' in encoding.environ:
117 if 'CHGHG' in encoding.environ:
118 ignored = {'HG'}
118 ignored = {'HG'}
119 else:
119 else:
120 ignored = set()
120 ignored = set()
121 envitems = [(k, v) for k, v in encoding.environ.iteritems()
121 envitems = [(k, v) for k, v in encoding.environ.iteritems()
122 if _envre.match(k) and k not in ignored]
122 if _envre.match(k) and k not in ignored]
123 envhash = _hashlist(sorted(envitems))
123 envhash = _hashlist(sorted(envitems))
124 return sectionhash[:6] + envhash[:6]
124 return sectionhash[:6] + envhash[:6]
125
125
126 def _getmtimepaths(ui):
126 def _getmtimepaths(ui):
127 """get a list of paths that should be checked to detect change
127 """get a list of paths that should be checked to detect change
128
128
129 The list will include:
129 The list will include:
130 - extensions (will not cover all files for complex extensions)
130 - extensions (will not cover all files for complex extensions)
131 - mercurial/__version__.py
131 - mercurial/__version__.py
132 - python binary
132 - python binary
133 """
133 """
134 modules = [m for n, m in extensions.extensions(ui)]
134 modules = [m for n, m in extensions.extensions(ui)]
135 try:
135 try:
136 from . import __version__
136 from . import __version__
137 modules.append(__version__)
137 modules.append(__version__)
138 except ImportError:
138 except ImportError:
139 pass
139 pass
140 files = [pycompat.sysexecutable]
140 files = [pycompat.sysexecutable]
141 for m in modules:
141 for m in modules:
142 try:
142 try:
143 files.append(inspect.getabsfile(m))
143 files.append(inspect.getabsfile(m))
144 except TypeError:
144 except TypeError:
145 pass
145 pass
146 return sorted(set(files))
146 return sorted(set(files))
147
147
148 def _mtimehash(paths):
148 def _mtimehash(paths):
149 """return a quick hash for detecting file changes
149 """return a quick hash for detecting file changes
150
150
151 mtimehash calls stat on given paths and calculate a hash based on size and
151 mtimehash calls stat on given paths and calculate a hash based on size and
152 mtime of each file. mtimehash does not read file content because reading is
152 mtime of each file. mtimehash does not read file content because reading is
153 expensive. therefore it's not 100% reliable for detecting content changes.
153 expensive. therefore it's not 100% reliable for detecting content changes.
154 it's possible to return different hashes for same file contents.
154 it's possible to return different hashes for same file contents.
155 it's also possible to return a same hash for different file contents for
155 it's also possible to return a same hash for different file contents for
156 some carefully crafted situation.
156 some carefully crafted situation.
157
157
158 for chgserver, it is designed that once mtimehash changes, the server is
158 for chgserver, it is designed that once mtimehash changes, the server is
159 considered outdated immediately and should no longer provide service.
159 considered outdated immediately and should no longer provide service.
160
160
161 mtimehash is not included in confighash because we only know the paths of
161 mtimehash is not included in confighash because we only know the paths of
162 extensions after importing them (there is imp.find_module but that faces
162 extensions after importing them (there is imp.find_module but that faces
163 race conditions). We need to calculate confighash without importing.
163 race conditions). We need to calculate confighash without importing.
164 """
164 """
165 def trystat(path):
165 def trystat(path):
166 try:
166 try:
167 st = os.stat(path)
167 st = os.stat(path)
168 return (st[stat.ST_MTIME], st.st_size)
168 return (st[stat.ST_MTIME], st.st_size)
169 except OSError:
169 except OSError:
170 # could be ENOENT, EPERM etc. not fatal in any case
170 # could be ENOENT, EPERM etc. not fatal in any case
171 pass
171 pass
172 return _hashlist(map(trystat, paths))[:12]
172 return _hashlist(map(trystat, paths))[:12]
173
173
174 class hashstate(object):
174 class hashstate(object):
175 """a structure storing confighash, mtimehash, paths used for mtimehash"""
175 """a structure storing confighash, mtimehash, paths used for mtimehash"""
176 def __init__(self, confighash, mtimehash, mtimepaths):
176 def __init__(self, confighash, mtimehash, mtimepaths):
177 self.confighash = confighash
177 self.confighash = confighash
178 self.mtimehash = mtimehash
178 self.mtimehash = mtimehash
179 self.mtimepaths = mtimepaths
179 self.mtimepaths = mtimepaths
180
180
181 @staticmethod
181 @staticmethod
182 def fromui(ui, mtimepaths=None):
182 def fromui(ui, mtimepaths=None):
183 if mtimepaths is None:
183 if mtimepaths is None:
184 mtimepaths = _getmtimepaths(ui)
184 mtimepaths = _getmtimepaths(ui)
185 confighash = _confighash(ui)
185 confighash = _confighash(ui)
186 mtimehash = _mtimehash(mtimepaths)
186 mtimehash = _mtimehash(mtimepaths)
187 ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
187 ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
188 confighash, mtimehash)
188 confighash, mtimehash)
189 return hashstate(confighash, mtimehash, mtimepaths)
189 return hashstate(confighash, mtimehash, mtimepaths)
190
190
191 def _newchgui(srcui, csystem, attachio):
191 def _newchgui(srcui, csystem, attachio):
192 class chgui(srcui.__class__):
192 class chgui(srcui.__class__):
193 def __init__(self, src=None):
193 def __init__(self, src=None):
194 super(chgui, self).__init__(src)
194 super(chgui, self).__init__(src)
195 if src:
195 if src:
196 self._csystem = getattr(src, '_csystem', csystem)
196 self._csystem = getattr(src, '_csystem', csystem)
197 else:
197 else:
198 self._csystem = csystem
198 self._csystem = csystem
199
199
200 def _runsystem(self, cmd, environ, cwd, out):
200 def _runsystem(self, cmd, environ, cwd, out):
201 # fallback to the original system method if
201 # fallback to the original system method if
202 # a. the output stream is not stdout (e.g. stderr, cStringIO),
202 # a. the output stream is not stdout (e.g. stderr, cStringIO),
203 # b. or stdout is redirected by protectstdio(),
203 # b. or stdout is redirected by protectstdio(),
204 # because the chg client is not aware of these situations and
204 # because the chg client is not aware of these situations and
205 # will behave differently (i.e. write to stdout).
205 # will behave differently (i.e. write to stdout).
206 if (out is not self.fout
206 if (out is not self.fout
207 or not util.safehasattr(self.fout, 'fileno')
207 or not util.safehasattr(self.fout, 'fileno')
208 or self.fout.fileno() != procutil.stdout.fileno()
208 or self.fout.fileno() != procutil.stdout.fileno()
209 or self._finoutredirected):
209 or self._finoutredirected):
210 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
210 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
211 self.flush()
211 self.flush()
212 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
212 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
213
213
214 def _runpager(self, cmd, env=None):
214 def _runpager(self, cmd, env=None):
215 self._csystem(cmd, procutil.shellenviron(env), type='pager',
215 self._csystem(cmd, procutil.shellenviron(env), type='pager',
216 cmdtable={'attachio': attachio})
216 cmdtable={'attachio': attachio})
217 return True
217 return True
218
218
219 return chgui(srcui)
219 return chgui(srcui)
220
220
221 def _loadnewui(srcui, args, cdebug):
221 def _loadnewui(srcui, args, cdebug):
222 from . import dispatch # avoid cycle
222 from . import dispatch # avoid cycle
223
223
224 newui = srcui.__class__.load()
224 newui = srcui.__class__.load()
225 for a in ['fin', 'fout', 'ferr', 'environ']:
225 for a in ['fin', 'fout', 'ferr', 'environ']:
226 setattr(newui, a, getattr(srcui, a))
226 setattr(newui, a, getattr(srcui, a))
227 if util.safehasattr(srcui, '_csystem'):
227 if util.safehasattr(srcui, '_csystem'):
228 newui._csystem = srcui._csystem
228 newui._csystem = srcui._csystem
229
229
230 # command line args
230 # command line args
231 options = dispatch._earlyparseopts(newui, args)
231 options = dispatch._earlyparseopts(newui, args)
232 dispatch._parseconfig(newui, options['config'])
232 dispatch._parseconfig(newui, options['config'])
233
233
234 # stolen from tortoisehg.util.copydynamicconfig()
234 # stolen from tortoisehg.util.copydynamicconfig()
235 for section, name, value in srcui.walkconfig():
235 for section, name, value in srcui.walkconfig():
236 source = srcui.configsource(section, name)
236 source = srcui.configsource(section, name)
237 if ':' in source or source == '--config' or source.startswith('$'):
237 if ':' in source or source == '--config' or source.startswith('$'):
238 # path:line or command line, or environ
238 # path:line or command line, or environ
239 continue
239 continue
240 newui.setconfig(section, name, value, source)
240 newui.setconfig(section, name, value, source)
241
241
242 # load wd and repo config, copied from dispatch.py
242 # load wd and repo config, copied from dispatch.py
243 cwd = options['cwd']
243 cwd = options['cwd']
244 cwd = cwd and os.path.realpath(cwd) or None
244 cwd = cwd and os.path.realpath(cwd) or None
245 rpath = options['repository']
245 rpath = options['repository']
246 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
246 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
247
247
248 extensions.populateui(newui)
248 extensions.populateui(newui)
249 commandserver.setuplogging(newui, fp=cdebug)
249 commandserver.setuplogging(newui, fp=cdebug)
250 if newui is not newlui:
250 if newui is not newlui:
251 extensions.populateui(newlui)
251 extensions.populateui(newlui)
252 commandserver.setuplogging(newlui, fp=cdebug)
252 commandserver.setuplogging(newlui, fp=cdebug)
253
253
254 return (newui, newlui)
254 return (newui, newlui)
255
255
256 class channeledsystem(object):
256 class channeledsystem(object):
257 """Propagate ui.system() request in the following format:
257 """Propagate ui.system() request in the following format:
258
258
259 payload length (unsigned int),
259 payload length (unsigned int),
260 type, '\0',
260 type, '\0',
261 cmd, '\0',
261 cmd, '\0',
262 cwd, '\0',
262 cwd, '\0',
263 envkey, '=', val, '\0',
263 envkey, '=', val, '\0',
264 ...
264 ...
265 envkey, '=', val
265 envkey, '=', val
266
266
267 if type == 'system', waits for:
267 if type == 'system', waits for:
268
268
269 exitcode length (unsigned int),
269 exitcode length (unsigned int),
270 exitcode (int)
270 exitcode (int)
271
271
272 if type == 'pager', repetitively waits for a command name ending with '\n'
272 if type == 'pager', repetitively waits for a command name ending with '\n'
273 and executes it defined by cmdtable, or exits the loop if the command name
273 and executes it defined by cmdtable, or exits the loop if the command name
274 is empty.
274 is empty.
275 """
275 """
276 def __init__(self, in_, out, channel):
276 def __init__(self, in_, out, channel):
277 self.in_ = in_
277 self.in_ = in_
278 self.out = out
278 self.out = out
279 self.channel = channel
279 self.channel = channel
280
280
281 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
281 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
282 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
282 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
283 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
283 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
284 data = '\0'.join(args)
284 data = '\0'.join(args)
285 self.out.write(struct.pack('>cI', self.channel, len(data)))
285 self.out.write(struct.pack('>cI', self.channel, len(data)))
286 self.out.write(data)
286 self.out.write(data)
287 self.out.flush()
287 self.out.flush()
288
288
289 if type == 'system':
289 if type == 'system':
290 length = self.in_.read(4)
290 length = self.in_.read(4)
291 length, = struct.unpack('>I', length)
291 length, = struct.unpack('>I', length)
292 if length != 4:
292 if length != 4:
293 raise error.Abort(_('invalid response'))
293 raise error.Abort(_('invalid response'))
294 rc, = struct.unpack('>i', self.in_.read(4))
294 rc, = struct.unpack('>i', self.in_.read(4))
295 return rc
295 return rc
296 elif type == 'pager':
296 elif type == 'pager':
297 while True:
297 while True:
298 cmd = self.in_.readline()[:-1]
298 cmd = self.in_.readline()[:-1]
299 if not cmd:
299 if not cmd:
300 break
300 break
301 if cmdtable and cmd in cmdtable:
301 if cmdtable and cmd in cmdtable:
302 cmdtable[cmd]()
302 cmdtable[cmd]()
303 else:
303 else:
304 raise error.Abort(_('unexpected command: %s') % cmd)
304 raise error.Abort(_('unexpected command: %s') % cmd)
305 else:
305 else:
306 raise error.ProgrammingError('invalid S channel type: %s' % type)
306 raise error.ProgrammingError('invalid S channel type: %s' % type)
307
307
308 _iochannels = [
308 _iochannels = [
309 # server.ch, ui.fp, mode
309 # server.ch, ui.fp, mode
310 ('cin', 'fin', r'rb'),
310 ('cin', 'fin', r'rb'),
311 ('cout', 'fout', r'wb'),
311 ('cout', 'fout', r'wb'),
312 ('cerr', 'ferr', r'wb'),
312 ('cerr', 'ferr', r'wb'),
313 ]
313 ]
314
314
315 class chgcmdserver(commandserver.server):
315 class chgcmdserver(commandserver.server):
316 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
316 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
317 super(chgcmdserver, self).__init__(
317 super(chgcmdserver, self).__init__(
318 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
318 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
319 repo, fin, fout)
319 repo, fin, fout)
320 self.clientsock = sock
320 self.clientsock = sock
321 self._ioattached = False
321 self._ioattached = False
322 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
322 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
323 self.hashstate = hashstate
323 self.hashstate = hashstate
324 self.baseaddress = baseaddress
324 self.baseaddress = baseaddress
325 if hashstate is not None:
325 if hashstate is not None:
326 self.capabilities = self.capabilities.copy()
326 self.capabilities = self.capabilities.copy()
327 self.capabilities['validate'] = chgcmdserver.validate
327 self.capabilities['validate'] = chgcmdserver.validate
328
328
329 def cleanup(self):
329 def cleanup(self):
330 super(chgcmdserver, self).cleanup()
330 super(chgcmdserver, self).cleanup()
331 # dispatch._runcatch() does not flush outputs if exception is not
331 # dispatch._runcatch() does not flush outputs if exception is not
332 # handled by dispatch._dispatch()
332 # handled by dispatch._dispatch()
333 self.ui.flush()
333 self.ui.flush()
334 self._restoreio()
334 self._restoreio()
335 self._ioattached = False
335 self._ioattached = False
336
336
337 def attachio(self):
337 def attachio(self):
338 """Attach to client's stdio passed via unix domain socket; all
338 """Attach to client's stdio passed via unix domain socket; all
339 channels except cresult will no longer be used
339 channels except cresult will no longer be used
340 """
340 """
341 # tell client to sendmsg() with 1-byte payload, which makes it
341 # tell client to sendmsg() with 1-byte payload, which makes it
342 # distinctive from "attachio\n" command consumed by client.read()
342 # distinctive from "attachio\n" command consumed by client.read()
343 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
343 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
344 clientfds = util.recvfds(self.clientsock.fileno())
344 clientfds = util.recvfds(self.clientsock.fileno())
345 self.ui.log('chgserver', 'received fds: %r\n', clientfds)
345 self.ui.log('chgserver', 'received fds: %r\n', clientfds)
346
346
347 ui = self.ui
347 ui = self.ui
348 ui.flush()
348 ui.flush()
349 self._saveio()
349 self._saveio()
350 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
350 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
351 assert fd > 0
351 assert fd > 0
352 fp = getattr(ui, fn)
352 fp = getattr(ui, fn)
353 os.dup2(fd, fp.fileno())
353 os.dup2(fd, fp.fileno())
354 os.close(fd)
354 os.close(fd)
355 if self._ioattached:
355 if self._ioattached:
356 continue
356 continue
357 # reset buffering mode when client is first attached. as we want
357 # reset buffering mode when client is first attached. as we want
358 # to see output immediately on pager, the mode stays unchanged
358 # to see output immediately on pager, the mode stays unchanged
359 # when client re-attached. ferr is unchanged because it should
359 # when client re-attached. ferr is unchanged because it should
360 # be unbuffered no matter if it is a tty or not.
360 # be unbuffered no matter if it is a tty or not.
361 if fn == 'ferr':
361 if fn == 'ferr':
362 newfp = fp
362 newfp = fp
363 else:
363 else:
364 # make it line buffered explicitly because the default is
364 # make it line buffered explicitly because the default is
365 # decided on first write(), where fout could be a pager.
365 # decided on first write(), where fout could be a pager.
366 if fp.isatty():
366 if fp.isatty():
367 bufsize = 1 # line buffered
367 bufsize = 1 # line buffered
368 else:
368 else:
369 bufsize = -1 # system default
369 bufsize = -1 # system default
370 newfp = os.fdopen(fp.fileno(), mode, bufsize)
370 newfp = os.fdopen(fp.fileno(), mode, bufsize)
371 setattr(ui, fn, newfp)
371 setattr(ui, fn, newfp)
372 setattr(self, cn, newfp)
372 setattr(self, cn, newfp)
373
373
374 self._ioattached = True
374 self._ioattached = True
375 self.cresult.write(struct.pack('>i', len(clientfds)))
375 self.cresult.write(struct.pack('>i', len(clientfds)))
376
376
377 def _saveio(self):
377 def _saveio(self):
378 if self._oldios:
378 if self._oldios:
379 return
379 return
380 ui = self.ui
380 ui = self.ui
381 for cn, fn, _mode in _iochannels:
381 for cn, fn, _mode in _iochannels:
382 ch = getattr(self, cn)
382 ch = getattr(self, cn)
383 fp = getattr(ui, fn)
383 fp = getattr(ui, fn)
384 fd = os.dup(fp.fileno())
384 fd = os.dup(fp.fileno())
385 self._oldios.append((ch, fp, fd))
385 self._oldios.append((ch, fp, fd))
386
386
387 def _restoreio(self):
387 def _restoreio(self):
388 ui = self.ui
388 ui = self.ui
389 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
389 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
390 newfp = getattr(ui, fn)
390 newfp = getattr(ui, fn)
391 # close newfp while it's associated with client; otherwise it
391 # close newfp while it's associated with client; otherwise it
392 # would be closed when newfp is deleted
392 # would be closed when newfp is deleted
393 if newfp is not fp:
393 if newfp is not fp:
394 newfp.close()
394 newfp.close()
395 # restore original fd: fp is open again
395 # restore original fd: fp is open again
396 os.dup2(fd, fp.fileno())
396 os.dup2(fd, fp.fileno())
397 os.close(fd)
397 os.close(fd)
398 setattr(self, cn, ch)
398 setattr(self, cn, ch)
399 setattr(ui, fn, fp)
399 setattr(ui, fn, fp)
400 del self._oldios[:]
400 del self._oldios[:]
401
401
402 def validate(self):
402 def validate(self):
403 """Reload the config and check if the server is up to date
403 """Reload the config and check if the server is up to date
404
404
405 Read a list of '\0' separated arguments.
405 Read a list of '\0' separated arguments.
406 Write a non-empty list of '\0' separated instruction strings or '\0'
406 Write a non-empty list of '\0' separated instruction strings or '\0'
407 if the list is empty.
407 if the list is empty.
408 An instruction string could be either:
408 An instruction string could be either:
409 - "unlink $path", the client should unlink the path to stop the
409 - "unlink $path", the client should unlink the path to stop the
410 outdated server.
410 outdated server.
411 - "redirect $path", the client should attempt to connect to $path
411 - "redirect $path", the client should attempt to connect to $path
412 first. If it does not work, start a new server. It implies
412 first. If it does not work, start a new server. It implies
413 "reconnect".
413 "reconnect".
414 - "exit $n", the client should exit directly with code n.
414 - "exit $n", the client should exit directly with code n.
415 This may happen if we cannot parse the config.
415 This may happen if we cannot parse the config.
416 - "reconnect", the client should close the connection and
416 - "reconnect", the client should close the connection and
417 reconnect.
417 reconnect.
418 If neither "reconnect" nor "redirect" is included in the instruction
418 If neither "reconnect" nor "redirect" is included in the instruction
419 list, the client can continue with this server after completing all
419 list, the client can continue with this server after completing all
420 the instructions.
420 the instructions.
421 """
421 """
422 from . import dispatch # avoid cycle
422 from . import dispatch # avoid cycle
423
423
424 args = self._readlist()
424 args = self._readlist()
425 try:
425 try:
426 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
426 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
427 except error.ParseError as inst:
427 except error.ParseError as inst:
428 dispatch._formatparse(self.ui.warn, inst)
428 dispatch._formatparse(self.ui.warn, inst)
429 self.ui.flush()
429 self.ui.flush()
430 self.cresult.write('exit 255')
430 self.cresult.write('exit 255')
431 return
431 return
432 except error.Abort as inst:
432 except error.Abort as inst:
433 self.ui.error(_("abort: %s\n") % inst)
433 self.ui.error(_("abort: %s\n") % inst)
434 if inst.hint:
434 if inst.hint:
435 self.ui.error(_("(%s)\n") % inst.hint)
435 self.ui.error(_("(%s)\n") % inst.hint)
436 self.ui.flush()
436 self.ui.flush()
437 self.cresult.write('exit 255')
437 self.cresult.write('exit 255')
438 return
438 return
439 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
439 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
440 insts = []
440 insts = []
441 if newhash.mtimehash != self.hashstate.mtimehash:
441 if newhash.mtimehash != self.hashstate.mtimehash:
442 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
442 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
443 insts.append('unlink %s' % addr)
443 insts.append('unlink %s' % addr)
444 # mtimehash is empty if one or more extensions fail to load.
444 # mtimehash is empty if one or more extensions fail to load.
445 # to be compatible with hg, still serve the client this time.
445 # to be compatible with hg, still serve the client this time.
446 if self.hashstate.mtimehash:
446 if self.hashstate.mtimehash:
447 insts.append('reconnect')
447 insts.append('reconnect')
448 if newhash.confighash != self.hashstate.confighash:
448 if newhash.confighash != self.hashstate.confighash:
449 addr = _hashaddress(self.baseaddress, newhash.confighash)
449 addr = _hashaddress(self.baseaddress, newhash.confighash)
450 insts.append('redirect %s' % addr)
450 insts.append('redirect %s' % addr)
451 self.ui.log('chgserver', 'validate: %s\n', insts)
451 self.ui.log('chgserver', 'validate: %s\n', insts)
452 self.cresult.write('\0'.join(insts) or '\0')
452 self.cresult.write('\0'.join(insts) or '\0')
453
453
454 def chdir(self):
454 def chdir(self):
455 """Change current directory
455 """Change current directory
456
456
457 Note that the behavior of --cwd option is bit different from this.
457 Note that the behavior of --cwd option is bit different from this.
458 It does not affect --config parameter.
458 It does not affect --config parameter.
459 """
459 """
460 path = self._readstr()
460 path = self._readstr()
461 if not path:
461 if not path:
462 return
462 return
463 self.ui.log('chgserver', 'chdir to %r\n', path)
463 self.ui.log('chgserver', 'chdir to %r\n', path)
464 os.chdir(path)
464 os.chdir(path)
465
465
466 def setumask(self):
466 def setumask(self):
467 """Change umask (DEPRECATED)"""
467 """Change umask (DEPRECATED)"""
468 # BUG: this does not follow the message frame structure, but kept for
468 # BUG: this does not follow the message frame structure, but kept for
469 # backward compatibility with old chg clients for some time
469 # backward compatibility with old chg clients for some time
470 self._setumask(self._read(4))
470 self._setumask(self._read(4))
471
471
472 def setumask2(self):
472 def setumask2(self):
473 """Change umask"""
473 """Change umask"""
474 data = self._readstr()
474 data = self._readstr()
475 if len(data) != 4:
475 if len(data) != 4:
476 raise ValueError('invalid mask length in setumask2 request')
476 raise ValueError('invalid mask length in setumask2 request')
477 self._setumask(data)
477 self._setumask(data)
478
478
479 def _setumask(self, data):
479 def _setumask(self, data):
480 mask = struct.unpack('>I', data)[0]
480 mask = struct.unpack('>I', data)[0]
481 self.ui.log('chgserver', 'setumask %r\n', mask)
481 self.ui.log('chgserver', 'setumask %r\n', mask)
482 os.umask(mask)
482 os.umask(mask)
483
483
484 def runcommand(self):
484 def runcommand(self):
485 # pager may be attached within the runcommand session, which should
485 # pager may be attached within the runcommand session, which should
486 # be detached at the end of the session. otherwise the pager wouldn't
486 # be detached at the end of the session. otherwise the pager wouldn't
487 # receive EOF.
487 # receive EOF.
488 globaloldios = self._oldios
488 globaloldios = self._oldios
489 self._oldios = []
489 self._oldios = []
490 try:
490 try:
491 return super(chgcmdserver, self).runcommand()
491 return super(chgcmdserver, self).runcommand()
492 finally:
492 finally:
493 self._restoreio()
493 self._restoreio()
494 self._oldios = globaloldios
494 self._oldios = globaloldios
495
495
496 def setenv(self):
496 def setenv(self):
497 """Clear and update os.environ
497 """Clear and update os.environ
498
498
499 Note that not all variables can make an effect on the running process.
499 Note that not all variables can make an effect on the running process.
500 """
500 """
501 l = self._readlist()
501 l = self._readlist()
502 try:
502 try:
503 newenv = dict(s.split('=', 1) for s in l)
503 newenv = dict(s.split('=', 1) for s in l)
504 except ValueError:
504 except ValueError:
505 raise ValueError('unexpected value in setenv request')
505 raise ValueError('unexpected value in setenv request')
506 self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
506 self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
507 encoding.environ.clear()
507 encoding.environ.clear()
508 encoding.environ.update(newenv)
508 encoding.environ.update(newenv)
509
509
510 capabilities = commandserver.server.capabilities.copy()
510 capabilities = commandserver.server.capabilities.copy()
511 capabilities.update({'attachio': attachio,
511 capabilities.update({'attachio': attachio,
512 'chdir': chdir,
512 'chdir': chdir,
513 'runcommand': runcommand,
513 'runcommand': runcommand,
514 'setenv': setenv,
514 'setenv': setenv,
515 'setumask': setumask,
515 'setumask': setumask,
516 'setumask2': setumask2})
516 'setumask2': setumask2})
517
517
518 if util.safehasattr(procutil, 'setprocname'):
518 if util.safehasattr(procutil, 'setprocname'):
519 def setprocname(self):
519 def setprocname(self):
520 """Change process title"""
520 """Change process title"""
521 name = self._readstr()
521 name = self._readstr()
522 self.ui.log('chgserver', 'setprocname: %r\n', name)
522 self.ui.log('chgserver', 'setprocname: %r\n', name)
523 procutil.setprocname(name)
523 procutil.setprocname(name)
524 capabilities['setprocname'] = setprocname
524 capabilities['setprocname'] = setprocname
525
525
526 def _tempaddress(address):
526 def _tempaddress(address):
527 return '%s.%d.tmp' % (address, os.getpid())
527 return '%s.%d.tmp' % (address, os.getpid())
528
528
529 def _hashaddress(address, hashstr):
529 def _hashaddress(address, hashstr):
530 # if the basename of address contains '.', use only the left part. this
530 # if the basename of address contains '.', use only the left part. this
531 # makes it possible for the client to pass 'server.tmp$PID' and follow by
531 # makes it possible for the client to pass 'server.tmp$PID' and follow by
532 # an atomic rename to avoid locking when spawning new servers.
532 # an atomic rename to avoid locking when spawning new servers.
533 dirname, basename = os.path.split(address)
533 dirname, basename = os.path.split(address)
534 basename = basename.split('.', 1)[0]
534 basename = basename.split('.', 1)[0]
535 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
535 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
536
536
537 class chgunixservicehandler(object):
537 class chgunixservicehandler(object):
538 """Set of operations for chg services"""
538 """Set of operations for chg services"""
539
539
540 pollinterval = 1 # [sec]
540 pollinterval = 1 # [sec]
541
541
542 def __init__(self, ui):
542 def __init__(self, ui):
543 self.ui = ui
543 self.ui = ui
544 self._idletimeout = ui.configint('chgserver', 'idletimeout')
544 self._idletimeout = ui.configint('chgserver', 'idletimeout')
545 self._lastactive = time.time()
545 self._lastactive = time.time()
546
546
547 def bindsocket(self, sock, address):
547 def bindsocket(self, sock, address):
548 self._inithashstate(address)
548 self._inithashstate(address)
549 self._checkextensions()
549 self._checkextensions()
550 self._bind(sock)
550 self._bind(sock)
551 self._createsymlink()
551 self._createsymlink()
552 # no "listening at" message should be printed to simulate hg behavior
552 # no "listening at" message should be printed to simulate hg behavior
553
553
554 def _inithashstate(self, address):
554 def _inithashstate(self, address):
555 self._baseaddress = address
555 self._baseaddress = address
556 if self.ui.configbool('chgserver', 'skiphash'):
556 if self.ui.configbool('chgserver', 'skiphash'):
557 self._hashstate = None
557 self._hashstate = None
558 self._realaddress = address
558 self._realaddress = address
559 return
559 return
560 self._hashstate = hashstate.fromui(self.ui)
560 self._hashstate = hashstate.fromui(self.ui)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562
562
563 def _checkextensions(self):
563 def _checkextensions(self):
564 if not self._hashstate:
564 if not self._hashstate:
565 return
565 return
566 if extensions.notloaded():
566 if extensions.notloaded():
567 # one or more extensions failed to load. mtimehash becomes
567 # one or more extensions failed to load. mtimehash becomes
568 # meaningless because we do not know the paths of those extensions.
568 # meaningless because we do not know the paths of those extensions.
569 # set mtimehash to an illegal hash value to invalidate the server.
569 # set mtimehash to an illegal hash value to invalidate the server.
570 self._hashstate.mtimehash = ''
570 self._hashstate.mtimehash = ''
571
571
572 def _bind(self, sock):
572 def _bind(self, sock):
573 # use a unique temp address so we can stat the file and do ownership
573 # use a unique temp address so we can stat the file and do ownership
574 # check later
574 # check later
575 tempaddress = _tempaddress(self._realaddress)
575 tempaddress = _tempaddress(self._realaddress)
576 util.bindunixsocket(sock, tempaddress)
576 util.bindunixsocket(sock, tempaddress)
577 self._socketstat = os.stat(tempaddress)
577 self._socketstat = os.stat(tempaddress)
578 sock.listen(socket.SOMAXCONN)
578 sock.listen(socket.SOMAXCONN)
579 # rename will replace the old socket file if exists atomically. the
579 # rename will replace the old socket file if exists atomically. the
580 # old server will detect ownership change and exit.
580 # old server will detect ownership change and exit.
581 util.rename(tempaddress, self._realaddress)
581 util.rename(tempaddress, self._realaddress)
582
582
583 def _createsymlink(self):
583 def _createsymlink(self):
584 if self._baseaddress == self._realaddress:
584 if self._baseaddress == self._realaddress:
585 return
585 return
586 tempaddress = _tempaddress(self._baseaddress)
586 tempaddress = _tempaddress(self._baseaddress)
587 os.symlink(os.path.basename(self._realaddress), tempaddress)
587 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 util.rename(tempaddress, self._baseaddress)
588 util.rename(tempaddress, self._baseaddress)
589
589
590 def _issocketowner(self):
590 def _issocketowner(self):
591 try:
591 try:
592 st = os.stat(self._realaddress)
592 st = os.stat(self._realaddress)
593 return (st.st_ino == self._socketstat.st_ino and
593 return (st.st_ino == self._socketstat.st_ino and
594 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
594 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
595 except OSError:
595 except OSError:
596 return False
596 return False
597
597
598 def unlinksocket(self, address):
598 def unlinksocket(self, address):
599 if not self._issocketowner():
599 if not self._issocketowner():
600 return
600 return
601 # it is possible to have a race condition here that we may
601 # it is possible to have a race condition here that we may
602 # remove another server's socket file. but that's okay
602 # remove another server's socket file. but that's okay
603 # since that server will detect and exit automatically and
603 # since that server will detect and exit automatically and
604 # the client will start a new server on demand.
604 # the client will start a new server on demand.
605 util.tryunlink(self._realaddress)
605 util.tryunlink(self._realaddress)
606
606
607 def shouldexit(self):
607 def shouldexit(self):
608 if not self._issocketowner():
608 if not self._issocketowner():
609 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
609 self.ui.log(b'chgserver', b'%s is not owned, exiting.\n',
610 self._realaddress)
610 return True
611 return True
611 if time.time() - self._lastactive > self._idletimeout:
612 if time.time() - self._lastactive > self._idletimeout:
612 self.ui.debug('being idle too long. exiting.\n')
613 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
613 return True
614 return True
614 return False
615 return False
615
616
616 def newconnection(self):
617 def newconnection(self):
617 self._lastactive = time.time()
618 self._lastactive = time.time()
618
619
619 def createcmdserver(self, repo, conn, fin, fout):
620 def createcmdserver(self, repo, conn, fin, fout):
620 return chgcmdserver(self.ui, repo, fin, fout, conn,
621 return chgcmdserver(self.ui, repo, fin, fout, conn,
621 self._hashstate, self._baseaddress)
622 self._hashstate, self._baseaddress)
622
623
623 def chgunixservice(ui, repo, opts):
624 def chgunixservice(ui, repo, opts):
624 # CHGINTERNALMARK is set by chg client. It is an indication of things are
625 # CHGINTERNALMARK is set by chg client. It is an indication of things are
625 # started by chg so other code can do things accordingly, like disabling
626 # started by chg so other code can do things accordingly, like disabling
626 # demandimport or detecting chg client started by chg client. When executed
627 # demandimport or detecting chg client started by chg client. When executed
627 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
628 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
628 # environ cleaner.
629 # environ cleaner.
629 if 'CHGINTERNALMARK' in encoding.environ:
630 if 'CHGINTERNALMARK' in encoding.environ:
630 del encoding.environ['CHGINTERNALMARK']
631 del encoding.environ['CHGINTERNALMARK']
631
632
632 if repo:
633 if repo:
633 # one chgserver can serve multiple repos. drop repo information
634 # one chgserver can serve multiple repos. drop repo information
634 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
635 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
635 h = chgunixservicehandler(ui)
636 h = chgunixservicehandler(ui)
636 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
637 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,623 +1,624
1 # commandserver.py - communicate with Mercurial's API over a pipe
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import gc
11 import gc
12 import os
12 import os
13 import random
13 import random
14 import signal
14 import signal
15 import socket
15 import socket
16 import struct
16 import struct
17 import traceback
17 import traceback
18
18
19 try:
19 try:
20 import selectors
20 import selectors
21 selectors.BaseSelector
21 selectors.BaseSelector
22 except ImportError:
22 except ImportError:
23 from .thirdparty import selectors2 as selectors
23 from .thirdparty import selectors2 as selectors
24
24
25 from .i18n import _
25 from .i18n import _
26 from . import (
26 from . import (
27 encoding,
27 encoding,
28 error,
28 error,
29 loggingutil,
29 loggingutil,
30 pycompat,
30 pycompat,
31 util,
31 util,
32 vfs as vfsmod,
32 vfs as vfsmod,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 cborutil,
35 cborutil,
36 procutil,
36 procutil,
37 )
37 )
38
38
39 class channeledoutput(object):
39 class channeledoutput(object):
40 """
40 """
41 Write data to out in the following format:
41 Write data to out in the following format:
42
42
43 data length (unsigned int),
43 data length (unsigned int),
44 data
44 data
45 """
45 """
46 def __init__(self, out, channel):
46 def __init__(self, out, channel):
47 self.out = out
47 self.out = out
48 self.channel = channel
48 self.channel = channel
49
49
50 @property
50 @property
51 def name(self):
51 def name(self):
52 return '<%c-channel>' % self.channel
52 return '<%c-channel>' % self.channel
53
53
54 def write(self, data):
54 def write(self, data):
55 if not data:
55 if not data:
56 return
56 return
57 # single write() to guarantee the same atomicity as the underlying file
57 # single write() to guarantee the same atomicity as the underlying file
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
59 self.out.flush()
59 self.out.flush()
60
60
61 def __getattr__(self, attr):
61 def __getattr__(self, attr):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
63 raise AttributeError(attr)
63 raise AttributeError(attr)
64 return getattr(self.out, attr)
64 return getattr(self.out, attr)
65
65
66 class channeledmessage(object):
66 class channeledmessage(object):
67 """
67 """
68 Write encoded message and metadata to out in the following format:
68 Write encoded message and metadata to out in the following format:
69
69
70 data length (unsigned int),
70 data length (unsigned int),
71 encoded message and metadata, as a flat key-value dict.
71 encoded message and metadata, as a flat key-value dict.
72
72
73 Each message should have 'type' attribute. Messages of unknown type
73 Each message should have 'type' attribute. Messages of unknown type
74 should be ignored.
74 should be ignored.
75 """
75 """
76
76
77 # teach ui that write() can take **opts
77 # teach ui that write() can take **opts
78 structured = True
78 structured = True
79
79
80 def __init__(self, out, channel, encodename, encodefn):
80 def __init__(self, out, channel, encodename, encodefn):
81 self._cout = channeledoutput(out, channel)
81 self._cout = channeledoutput(out, channel)
82 self.encoding = encodename
82 self.encoding = encodename
83 self._encodefn = encodefn
83 self._encodefn = encodefn
84
84
85 def write(self, data, **opts):
85 def write(self, data, **opts):
86 opts = pycompat.byteskwargs(opts)
86 opts = pycompat.byteskwargs(opts)
87 if data is not None:
87 if data is not None:
88 opts[b'data'] = data
88 opts[b'data'] = data
89 self._cout.write(self._encodefn(opts))
89 self._cout.write(self._encodefn(opts))
90
90
91 def __getattr__(self, attr):
91 def __getattr__(self, attr):
92 return getattr(self._cout, attr)
92 return getattr(self._cout, attr)
93
93
94 class channeledinput(object):
94 class channeledinput(object):
95 """
95 """
96 Read data from in_.
96 Read data from in_.
97
97
98 Requests for input are written to out in the following format:
98 Requests for input are written to out in the following format:
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
100 how many bytes to send at most (unsigned int),
100 how many bytes to send at most (unsigned int),
101
101
102 The client replies with:
102 The client replies with:
103 data length (unsigned int), 0 meaning EOF
103 data length (unsigned int), 0 meaning EOF
104 data
104 data
105 """
105 """
106
106
107 maxchunksize = 4 * 1024
107 maxchunksize = 4 * 1024
108
108
109 def __init__(self, in_, out, channel):
109 def __init__(self, in_, out, channel):
110 self.in_ = in_
110 self.in_ = in_
111 self.out = out
111 self.out = out
112 self.channel = channel
112 self.channel = channel
113
113
114 @property
114 @property
115 def name(self):
115 def name(self):
116 return '<%c-channel>' % self.channel
116 return '<%c-channel>' % self.channel
117
117
118 def read(self, size=-1):
118 def read(self, size=-1):
119 if size < 0:
119 if size < 0:
120 # if we need to consume all the clients input, ask for 4k chunks
120 # if we need to consume all the clients input, ask for 4k chunks
121 # so the pipe doesn't fill up risking a deadlock
121 # so the pipe doesn't fill up risking a deadlock
122 size = self.maxchunksize
122 size = self.maxchunksize
123 s = self._read(size, self.channel)
123 s = self._read(size, self.channel)
124 buf = s
124 buf = s
125 while s:
125 while s:
126 s = self._read(size, self.channel)
126 s = self._read(size, self.channel)
127 buf += s
127 buf += s
128
128
129 return buf
129 return buf
130 else:
130 else:
131 return self._read(size, self.channel)
131 return self._read(size, self.channel)
132
132
133 def _read(self, size, channel):
133 def _read(self, size, channel):
134 if not size:
134 if not size:
135 return ''
135 return ''
136 assert size > 0
136 assert size > 0
137
137
138 # tell the client we need at most size bytes
138 # tell the client we need at most size bytes
139 self.out.write(struct.pack('>cI', channel, size))
139 self.out.write(struct.pack('>cI', channel, size))
140 self.out.flush()
140 self.out.flush()
141
141
142 length = self.in_.read(4)
142 length = self.in_.read(4)
143 length = struct.unpack('>I', length)[0]
143 length = struct.unpack('>I', length)[0]
144 if not length:
144 if not length:
145 return ''
145 return ''
146 else:
146 else:
147 return self.in_.read(length)
147 return self.in_.read(length)
148
148
149 def readline(self, size=-1):
149 def readline(self, size=-1):
150 if size < 0:
150 if size < 0:
151 size = self.maxchunksize
151 size = self.maxchunksize
152 s = self._read(size, 'L')
152 s = self._read(size, 'L')
153 buf = s
153 buf = s
154 # keep asking for more until there's either no more or
154 # keep asking for more until there's either no more or
155 # we got a full line
155 # we got a full line
156 while s and s[-1] != '\n':
156 while s and s[-1] != '\n':
157 s = self._read(size, 'L')
157 s = self._read(size, 'L')
158 buf += s
158 buf += s
159
159
160 return buf
160 return buf
161 else:
161 else:
162 return self._read(size, 'L')
162 return self._read(size, 'L')
163
163
164 def __iter__(self):
164 def __iter__(self):
165 return self
165 return self
166
166
167 def next(self):
167 def next(self):
168 l = self.readline()
168 l = self.readline()
169 if not l:
169 if not l:
170 raise StopIteration
170 raise StopIteration
171 return l
171 return l
172
172
173 __next__ = next
173 __next__ = next
174
174
175 def __getattr__(self, attr):
175 def __getattr__(self, attr):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
177 raise AttributeError(attr)
177 raise AttributeError(attr)
178 return getattr(self.in_, attr)
178 return getattr(self.in_, attr)
179
179
180 _messageencoders = {
180 _messageencoders = {
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
182 }
182 }
183
183
184 def _selectmessageencoder(ui):
184 def _selectmessageencoder(ui):
185 # experimental config: cmdserver.message-encodings
185 # experimental config: cmdserver.message-encodings
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
187 for n in encnames:
187 for n in encnames:
188 f = _messageencoders.get(n)
188 f = _messageencoders.get(n)
189 if f:
189 if f:
190 return n, f
190 return n, f
191 raise error.Abort(b'no supported message encodings: %s'
191 raise error.Abort(b'no supported message encodings: %s'
192 % b' '.join(encnames))
192 % b' '.join(encnames))
193
193
194 class server(object):
194 class server(object):
195 """
195 """
196 Listens for commands on fin, runs them and writes the output on a channel
196 Listens for commands on fin, runs them and writes the output on a channel
197 based stream to fout.
197 based stream to fout.
198 """
198 """
199 def __init__(self, ui, repo, fin, fout):
199 def __init__(self, ui, repo, fin, fout):
200 self.cwd = encoding.getcwd()
200 self.cwd = encoding.getcwd()
201
201
202 if repo:
202 if repo:
203 # the ui here is really the repo ui so take its baseui so we don't
203 # the ui here is really the repo ui so take its baseui so we don't
204 # end up with its local configuration
204 # end up with its local configuration
205 self.ui = repo.baseui
205 self.ui = repo.baseui
206 self.repo = repo
206 self.repo = repo
207 self.repoui = repo.ui
207 self.repoui = repo.ui
208 else:
208 else:
209 self.ui = ui
209 self.ui = ui
210 self.repo = self.repoui = None
210 self.repo = self.repoui = None
211
211
212 self.cdebug = channeledoutput(fout, 'd')
212 self.cdebug = channeledoutput(fout, 'd')
213 self.cerr = channeledoutput(fout, 'e')
213 self.cerr = channeledoutput(fout, 'e')
214 self.cout = channeledoutput(fout, 'o')
214 self.cout = channeledoutput(fout, 'o')
215 self.cin = channeledinput(fin, fout, 'I')
215 self.cin = channeledinput(fin, fout, 'I')
216 self.cresult = channeledoutput(fout, 'r')
216 self.cresult = channeledoutput(fout, 'r')
217
217
218 if self.ui.config(b'cmdserver', b'log') == b'-':
218 if self.ui.config(b'cmdserver', b'log') == b'-':
219 # switch log stream of server's ui to the 'd' (debug) channel
219 # switch log stream of server's ui to the 'd' (debug) channel
220 # (don't touch repo.ui as its lifetime is longer than the server)
220 # (don't touch repo.ui as its lifetime is longer than the server)
221 self.ui = self.ui.copy()
221 self.ui = self.ui.copy()
222 setuplogging(self.ui, repo=None, fp=self.cdebug)
222 setuplogging(self.ui, repo=None, fp=self.cdebug)
223
223
224 # TODO: add this to help/config.txt when stabilized
224 # TODO: add this to help/config.txt when stabilized
225 # ``channel``
225 # ``channel``
226 # Use separate channel for structured output. (Command-server only)
226 # Use separate channel for structured output. (Command-server only)
227 self.cmsg = None
227 self.cmsg = None
228 if ui.config(b'ui', b'message-output') == b'channel':
228 if ui.config(b'ui', b'message-output') == b'channel':
229 encname, encfn = _selectmessageencoder(ui)
229 encname, encfn = _selectmessageencoder(ui)
230 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
230 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
231
231
232 self.client = fin
232 self.client = fin
233
233
234 def cleanup(self):
234 def cleanup(self):
235 """release and restore resources taken during server session"""
235 """release and restore resources taken during server session"""
236
236
237 def _read(self, size):
237 def _read(self, size):
238 if not size:
238 if not size:
239 return ''
239 return ''
240
240
241 data = self.client.read(size)
241 data = self.client.read(size)
242
242
243 # is the other end closed?
243 # is the other end closed?
244 if not data:
244 if not data:
245 raise EOFError
245 raise EOFError
246
246
247 return data
247 return data
248
248
249 def _readstr(self):
249 def _readstr(self):
250 """read a string from the channel
250 """read a string from the channel
251
251
252 format:
252 format:
253 data length (uint32), data
253 data length (uint32), data
254 """
254 """
255 length = struct.unpack('>I', self._read(4))[0]
255 length = struct.unpack('>I', self._read(4))[0]
256 if not length:
256 if not length:
257 return ''
257 return ''
258 return self._read(length)
258 return self._read(length)
259
259
260 def _readlist(self):
260 def _readlist(self):
261 """read a list of NULL separated strings from the channel"""
261 """read a list of NULL separated strings from the channel"""
262 s = self._readstr()
262 s = self._readstr()
263 if s:
263 if s:
264 return s.split('\0')
264 return s.split('\0')
265 else:
265 else:
266 return []
266 return []
267
267
268 def runcommand(self):
268 def runcommand(self):
269 """ reads a list of \0 terminated arguments, executes
269 """ reads a list of \0 terminated arguments, executes
270 and writes the return code to the result channel """
270 and writes the return code to the result channel """
271 from . import dispatch # avoid cycle
271 from . import dispatch # avoid cycle
272
272
273 args = self._readlist()
273 args = self._readlist()
274
274
275 # copy the uis so changes (e.g. --config or --verbose) don't
275 # copy the uis so changes (e.g. --config or --verbose) don't
276 # persist between requests
276 # persist between requests
277 copiedui = self.ui.copy()
277 copiedui = self.ui.copy()
278 uis = [copiedui]
278 uis = [copiedui]
279 if self.repo:
279 if self.repo:
280 self.repo.baseui = copiedui
280 self.repo.baseui = copiedui
281 # clone ui without using ui.copy because this is protected
281 # clone ui without using ui.copy because this is protected
282 repoui = self.repoui.__class__(self.repoui)
282 repoui = self.repoui.__class__(self.repoui)
283 repoui.copy = copiedui.copy # redo copy protection
283 repoui.copy = copiedui.copy # redo copy protection
284 uis.append(repoui)
284 uis.append(repoui)
285 self.repo.ui = self.repo.dirstate._ui = repoui
285 self.repo.ui = self.repo.dirstate._ui = repoui
286 self.repo.invalidateall()
286 self.repo.invalidateall()
287
287
288 for ui in uis:
288 for ui in uis:
289 ui.resetstate()
289 ui.resetstate()
290 # any kind of interaction must use server channels, but chg may
290 # any kind of interaction must use server channels, but chg may
291 # replace channels by fully functional tty files. so nontty is
291 # replace channels by fully functional tty files. so nontty is
292 # enforced only if cin is a channel.
292 # enforced only if cin is a channel.
293 if not util.safehasattr(self.cin, 'fileno'):
293 if not util.safehasattr(self.cin, 'fileno'):
294 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
294 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
295
295
296 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
296 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
297 self.cout, self.cerr, self.cmsg)
297 self.cout, self.cerr, self.cmsg)
298
298
299 try:
299 try:
300 ret = dispatch.dispatch(req) & 255
300 ret = dispatch.dispatch(req) & 255
301 self.cresult.write(struct.pack('>i', int(ret)))
301 self.cresult.write(struct.pack('>i', int(ret)))
302 finally:
302 finally:
303 # restore old cwd
303 # restore old cwd
304 if '--cwd' in args:
304 if '--cwd' in args:
305 os.chdir(self.cwd)
305 os.chdir(self.cwd)
306
306
307 def getencoding(self):
307 def getencoding(self):
308 """ writes the current encoding to the result channel """
308 """ writes the current encoding to the result channel """
309 self.cresult.write(encoding.encoding)
309 self.cresult.write(encoding.encoding)
310
310
311 def serveone(self):
311 def serveone(self):
312 cmd = self.client.readline()[:-1]
312 cmd = self.client.readline()[:-1]
313 if cmd:
313 if cmd:
314 handler = self.capabilities.get(cmd)
314 handler = self.capabilities.get(cmd)
315 if handler:
315 if handler:
316 handler(self)
316 handler(self)
317 else:
317 else:
318 # clients are expected to check what commands are supported by
318 # clients are expected to check what commands are supported by
319 # looking at the servers capabilities
319 # looking at the servers capabilities
320 raise error.Abort(_('unknown command %s') % cmd)
320 raise error.Abort(_('unknown command %s') % cmd)
321
321
322 return cmd != ''
322 return cmd != ''
323
323
324 capabilities = {'runcommand': runcommand,
324 capabilities = {'runcommand': runcommand,
325 'getencoding': getencoding}
325 'getencoding': getencoding}
326
326
327 def serve(self):
327 def serve(self):
328 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
328 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
329 hellomsg += '\n'
329 hellomsg += '\n'
330 hellomsg += 'encoding: ' + encoding.encoding
330 hellomsg += 'encoding: ' + encoding.encoding
331 hellomsg += '\n'
331 hellomsg += '\n'
332 if self.cmsg:
332 if self.cmsg:
333 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
333 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
334 hellomsg += 'pid: %d' % procutil.getpid()
334 hellomsg += 'pid: %d' % procutil.getpid()
335 if util.safehasattr(os, 'getpgid'):
335 if util.safehasattr(os, 'getpgid'):
336 hellomsg += '\n'
336 hellomsg += '\n'
337 hellomsg += 'pgid: %d' % os.getpgid(0)
337 hellomsg += 'pgid: %d' % os.getpgid(0)
338
338
339 # write the hello msg in -one- chunk
339 # write the hello msg in -one- chunk
340 self.cout.write(hellomsg)
340 self.cout.write(hellomsg)
341
341
342 try:
342 try:
343 while self.serveone():
343 while self.serveone():
344 pass
344 pass
345 except EOFError:
345 except EOFError:
346 # we'll get here if the client disconnected while we were reading
346 # we'll get here if the client disconnected while we were reading
347 # its request
347 # its request
348 return 1
348 return 1
349
349
350 return 0
350 return 0
351
351
352 def setuplogging(ui, repo=None, fp=None):
352 def setuplogging(ui, repo=None, fp=None):
353 """Set up server logging facility
353 """Set up server logging facility
354
354
355 If cmdserver.log is '-', log messages will be sent to the given fp.
355 If cmdserver.log is '-', log messages will be sent to the given fp.
356 It should be the 'd' channel while a client is connected, and otherwise
356 It should be the 'd' channel while a client is connected, and otherwise
357 is the stderr of the server process.
357 is the stderr of the server process.
358 """
358 """
359 # developer config: cmdserver.log
359 # developer config: cmdserver.log
360 logpath = ui.config(b'cmdserver', b'log')
360 logpath = ui.config(b'cmdserver', b'log')
361 if not logpath:
361 if not logpath:
362 return
362 return
363 # developer config: cmdserver.track-log
363 # developer config: cmdserver.track-log
364 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
364 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
365
365
366 if logpath == b'-' and fp:
366 if logpath == b'-' and fp:
367 logger = loggingutil.fileobjectlogger(fp, tracked)
367 logger = loggingutil.fileobjectlogger(fp, tracked)
368 elif logpath == b'-':
368 elif logpath == b'-':
369 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
369 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
370 else:
370 else:
371 logpath = os.path.abspath(util.expandpath(logpath))
371 logpath = os.path.abspath(util.expandpath(logpath))
372 # developer config: cmdserver.max-log-files
372 # developer config: cmdserver.max-log-files
373 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
373 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
374 # developer config: cmdserver.max-log-size
374 # developer config: cmdserver.max-log-size
375 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
375 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
376 vfs = vfsmod.vfs(os.path.dirname(logpath))
376 vfs = vfsmod.vfs(os.path.dirname(logpath))
377 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
377 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
378 maxfiles=maxfiles, maxsize=maxsize)
378 maxfiles=maxfiles, maxsize=maxsize)
379
379
380 targetuis = {ui}
380 targetuis = {ui}
381 if repo:
381 if repo:
382 targetuis.add(repo.baseui)
382 targetuis.add(repo.baseui)
383 targetuis.add(repo.ui)
383 targetuis.add(repo.ui)
384 for u in targetuis:
384 for u in targetuis:
385 u.setlogger(b'cmdserver', logger)
385 u.setlogger(b'cmdserver', logger)
386
386
387 class pipeservice(object):
387 class pipeservice(object):
388 def __init__(self, ui, repo, opts):
388 def __init__(self, ui, repo, opts):
389 self.ui = ui
389 self.ui = ui
390 self.repo = repo
390 self.repo = repo
391
391
392 def init(self):
392 def init(self):
393 pass
393 pass
394
394
395 def run(self):
395 def run(self):
396 ui = self.ui
396 ui = self.ui
397 # redirect stdio to null device so that broken extensions or in-process
397 # redirect stdio to null device so that broken extensions or in-process
398 # hooks will never cause corruption of channel protocol.
398 # hooks will never cause corruption of channel protocol.
399 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
399 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
400 sv = server(ui, self.repo, fin, fout)
400 sv = server(ui, self.repo, fin, fout)
401 try:
401 try:
402 return sv.serve()
402 return sv.serve()
403 finally:
403 finally:
404 sv.cleanup()
404 sv.cleanup()
405
405
406 def _initworkerprocess():
406 def _initworkerprocess():
407 # use a different process group from the master process, in order to:
407 # use a different process group from the master process, in order to:
408 # 1. make the current process group no longer "orphaned" (because the
408 # 1. make the current process group no longer "orphaned" (because the
409 # parent of this process is in a different process group while
409 # parent of this process is in a different process group while
410 # remains in a same session)
410 # remains in a same session)
411 # according to POSIX 2.2.2.52, orphaned process group will ignore
411 # according to POSIX 2.2.2.52, orphaned process group will ignore
412 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
412 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
413 # cause trouble for things like ncurses.
413 # cause trouble for things like ncurses.
414 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
414 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
415 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
415 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
416 # processes like ssh will be killed properly, without affecting
416 # processes like ssh will be killed properly, without affecting
417 # unrelated processes.
417 # unrelated processes.
418 os.setpgid(0, 0)
418 os.setpgid(0, 0)
419 # change random state otherwise forked request handlers would have a
419 # change random state otherwise forked request handlers would have a
420 # same state inherited from parent.
420 # same state inherited from parent.
421 random.seed()
421 random.seed()
422
422
423 def _serverequest(ui, repo, conn, createcmdserver):
423 def _serverequest(ui, repo, conn, createcmdserver):
424 fin = conn.makefile(r'rb')
424 fin = conn.makefile(r'rb')
425 fout = conn.makefile(r'wb')
425 fout = conn.makefile(r'wb')
426 sv = None
426 sv = None
427 try:
427 try:
428 sv = createcmdserver(repo, conn, fin, fout)
428 sv = createcmdserver(repo, conn, fin, fout)
429 try:
429 try:
430 sv.serve()
430 sv.serve()
431 # handle exceptions that may be raised by command server. most of
431 # handle exceptions that may be raised by command server. most of
432 # known exceptions are caught by dispatch.
432 # known exceptions are caught by dispatch.
433 except error.Abort as inst:
433 except error.Abort as inst:
434 ui.error(_('abort: %s\n') % inst)
434 ui.error(_('abort: %s\n') % inst)
435 except IOError as inst:
435 except IOError as inst:
436 if inst.errno != errno.EPIPE:
436 if inst.errno != errno.EPIPE:
437 raise
437 raise
438 except KeyboardInterrupt:
438 except KeyboardInterrupt:
439 pass
439 pass
440 finally:
440 finally:
441 sv.cleanup()
441 sv.cleanup()
442 except: # re-raises
442 except: # re-raises
443 # also write traceback to error channel. otherwise client cannot
443 # also write traceback to error channel. otherwise client cannot
444 # see it because it is written to server's stderr by default.
444 # see it because it is written to server's stderr by default.
445 if sv:
445 if sv:
446 cerr = sv.cerr
446 cerr = sv.cerr
447 else:
447 else:
448 cerr = channeledoutput(fout, 'e')
448 cerr = channeledoutput(fout, 'e')
449 cerr.write(encoding.strtolocal(traceback.format_exc()))
449 cerr.write(encoding.strtolocal(traceback.format_exc()))
450 raise
450 raise
451 finally:
451 finally:
452 fin.close()
452 fin.close()
453 try:
453 try:
454 fout.close() # implicit flush() may cause another EPIPE
454 fout.close() # implicit flush() may cause another EPIPE
455 except IOError as inst:
455 except IOError as inst:
456 if inst.errno != errno.EPIPE:
456 if inst.errno != errno.EPIPE:
457 raise
457 raise
458
458
459 class unixservicehandler(object):
459 class unixservicehandler(object):
460 """Set of pluggable operations for unix-mode services
460 """Set of pluggable operations for unix-mode services
461
461
462 Almost all methods except for createcmdserver() are called in the main
462 Almost all methods except for createcmdserver() are called in the main
463 process. You can't pass mutable resource back from createcmdserver().
463 process. You can't pass mutable resource back from createcmdserver().
464 """
464 """
465
465
466 pollinterval = None
466 pollinterval = None
467
467
468 def __init__(self, ui):
468 def __init__(self, ui):
469 self.ui = ui
469 self.ui = ui
470
470
471 def bindsocket(self, sock, address):
471 def bindsocket(self, sock, address):
472 util.bindunixsocket(sock, address)
472 util.bindunixsocket(sock, address)
473 sock.listen(socket.SOMAXCONN)
473 sock.listen(socket.SOMAXCONN)
474 self.ui.status(_('listening at %s\n') % address)
474 self.ui.status(_('listening at %s\n') % address)
475 self.ui.flush() # avoid buffering of status message
475 self.ui.flush() # avoid buffering of status message
476
476
477 def unlinksocket(self, address):
477 def unlinksocket(self, address):
478 os.unlink(address)
478 os.unlink(address)
479
479
480 def shouldexit(self):
480 def shouldexit(self):
481 """True if server should shut down; checked per pollinterval"""
481 """True if server should shut down; checked per pollinterval"""
482 return False
482 return False
483
483
484 def newconnection(self):
484 def newconnection(self):
485 """Called when main process notices new connection"""
485 """Called when main process notices new connection"""
486
486
487 def createcmdserver(self, repo, conn, fin, fout):
487 def createcmdserver(self, repo, conn, fin, fout):
488 """Create new command server instance; called in the process that
488 """Create new command server instance; called in the process that
489 serves for the current connection"""
489 serves for the current connection"""
490 return server(self.ui, repo, fin, fout)
490 return server(self.ui, repo, fin, fout)
491
491
492 class unixforkingservice(object):
492 class unixforkingservice(object):
493 """
493 """
494 Listens on unix domain socket and forks server per connection
494 Listens on unix domain socket and forks server per connection
495 """
495 """
496
496
497 def __init__(self, ui, repo, opts, handler=None):
497 def __init__(self, ui, repo, opts, handler=None):
498 self.ui = ui
498 self.ui = ui
499 self.repo = repo
499 self.repo = repo
500 self.address = opts['address']
500 self.address = opts['address']
501 if not util.safehasattr(socket, 'AF_UNIX'):
501 if not util.safehasattr(socket, 'AF_UNIX'):
502 raise error.Abort(_('unsupported platform'))
502 raise error.Abort(_('unsupported platform'))
503 if not self.address:
503 if not self.address:
504 raise error.Abort(_('no socket path specified with --address'))
504 raise error.Abort(_('no socket path specified with --address'))
505 self._servicehandler = handler or unixservicehandler(ui)
505 self._servicehandler = handler or unixservicehandler(ui)
506 self._sock = None
506 self._sock = None
507 self._oldsigchldhandler = None
507 self._oldsigchldhandler = None
508 self._workerpids = set() # updated by signal handler; do not iterate
508 self._workerpids = set() # updated by signal handler; do not iterate
509 self._socketunlinked = None
509 self._socketunlinked = None
510
510
511 def init(self):
511 def init(self):
512 self._sock = socket.socket(socket.AF_UNIX)
512 self._sock = socket.socket(socket.AF_UNIX)
513 self._servicehandler.bindsocket(self._sock, self.address)
513 self._servicehandler.bindsocket(self._sock, self.address)
514 if util.safehasattr(procutil, 'unblocksignal'):
514 if util.safehasattr(procutil, 'unblocksignal'):
515 procutil.unblocksignal(signal.SIGCHLD)
515 procutil.unblocksignal(signal.SIGCHLD)
516 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
516 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
517 self._oldsigchldhandler = o
517 self._oldsigchldhandler = o
518 self._socketunlinked = False
518 self._socketunlinked = False
519
519
520 def _unlinksocket(self):
520 def _unlinksocket(self):
521 if not self._socketunlinked:
521 if not self._socketunlinked:
522 self._servicehandler.unlinksocket(self.address)
522 self._servicehandler.unlinksocket(self.address)
523 self._socketunlinked = True
523 self._socketunlinked = True
524
524
525 def _cleanup(self):
525 def _cleanup(self):
526 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
526 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
527 self._sock.close()
527 self._sock.close()
528 self._unlinksocket()
528 self._unlinksocket()
529 # don't kill child processes as they have active clients, just wait
529 # don't kill child processes as they have active clients, just wait
530 self._reapworkers(0)
530 self._reapworkers(0)
531
531
532 def run(self):
532 def run(self):
533 try:
533 try:
534 self._mainloop()
534 self._mainloop()
535 finally:
535 finally:
536 self._cleanup()
536 self._cleanup()
537
537
538 def _mainloop(self):
538 def _mainloop(self):
539 exiting = False
539 exiting = False
540 h = self._servicehandler
540 h = self._servicehandler
541 selector = selectors.DefaultSelector()
541 selector = selectors.DefaultSelector()
542 selector.register(self._sock, selectors.EVENT_READ)
542 selector.register(self._sock, selectors.EVENT_READ)
543 while True:
543 while True:
544 if not exiting and h.shouldexit():
544 if not exiting and h.shouldexit():
545 # clients can no longer connect() to the domain socket, so
545 # clients can no longer connect() to the domain socket, so
546 # we stop queuing new requests.
546 # we stop queuing new requests.
547 # for requests that are queued (connect()-ed, but haven't been
547 # for requests that are queued (connect()-ed, but haven't been
548 # accept()-ed), handle them before exit. otherwise, clients
548 # accept()-ed), handle them before exit. otherwise, clients
549 # waiting for recv() will receive ECONNRESET.
549 # waiting for recv() will receive ECONNRESET.
550 self._unlinksocket()
550 self._unlinksocket()
551 exiting = True
551 exiting = True
552 try:
552 try:
553 ready = selector.select(timeout=h.pollinterval)
553 ready = selector.select(timeout=h.pollinterval)
554 except OSError as inst:
554 except OSError as inst:
555 # selectors2 raises ETIMEDOUT if timeout exceeded while
555 # selectors2 raises ETIMEDOUT if timeout exceeded while
556 # handling signal interrupt. That's probably wrong, but
556 # handling signal interrupt. That's probably wrong, but
557 # we can easily get around it.
557 # we can easily get around it.
558 if inst.errno != errno.ETIMEDOUT:
558 if inst.errno != errno.ETIMEDOUT:
559 raise
559 raise
560 ready = []
560 ready = []
561 if not ready:
561 if not ready:
562 # only exit if we completed all queued requests
562 # only exit if we completed all queued requests
563 if exiting:
563 if exiting:
564 break
564 break
565 continue
565 continue
566 try:
566 try:
567 conn, _addr = self._sock.accept()
567 conn, _addr = self._sock.accept()
568 except socket.error as inst:
568 except socket.error as inst:
569 if inst.args[0] == errno.EINTR:
569 if inst.args[0] == errno.EINTR:
570 continue
570 continue
571 raise
571 raise
572
572
573 pid = os.fork()
573 pid = os.fork()
574 if pid:
574 if pid:
575 try:
575 try:
576 self.ui.debug('forked worker process (pid=%d)\n' % pid)
576 self.ui.log(b'cmdserver',
577 b'forked worker process (pid=%d)\n', pid)
577 self._workerpids.add(pid)
578 self._workerpids.add(pid)
578 h.newconnection()
579 h.newconnection()
579 finally:
580 finally:
580 conn.close() # release handle in parent process
581 conn.close() # release handle in parent process
581 else:
582 else:
582 try:
583 try:
583 selector.close()
584 selector.close()
584 self._sock.close()
585 self._sock.close()
585 self._runworker(conn)
586 self._runworker(conn)
586 conn.close()
587 conn.close()
587 os._exit(0)
588 os._exit(0)
588 except: # never return, hence no re-raises
589 except: # never return, hence no re-raises
589 try:
590 try:
590 self.ui.traceback(force=True)
591 self.ui.traceback(force=True)
591 finally:
592 finally:
592 os._exit(255)
593 os._exit(255)
593 selector.close()
594 selector.close()
594
595
595 def _sigchldhandler(self, signal, frame):
596 def _sigchldhandler(self, signal, frame):
596 self._reapworkers(os.WNOHANG)
597 self._reapworkers(os.WNOHANG)
597
598
598 def _reapworkers(self, options):
599 def _reapworkers(self, options):
599 while self._workerpids:
600 while self._workerpids:
600 try:
601 try:
601 pid, _status = os.waitpid(-1, options)
602 pid, _status = os.waitpid(-1, options)
602 except OSError as inst:
603 except OSError as inst:
603 if inst.errno == errno.EINTR:
604 if inst.errno == errno.EINTR:
604 continue
605 continue
605 if inst.errno != errno.ECHILD:
606 if inst.errno != errno.ECHILD:
606 raise
607 raise
607 # no child processes at all (reaped by other waitpid()?)
608 # no child processes at all (reaped by other waitpid()?)
608 self._workerpids.clear()
609 self._workerpids.clear()
609 return
610 return
610 if pid == 0:
611 if pid == 0:
611 # no waitable child processes
612 # no waitable child processes
612 return
613 return
613 self.ui.debug('worker process exited (pid=%d)\n' % pid)
614 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
614 self._workerpids.discard(pid)
615 self._workerpids.discard(pid)
615
616
616 def _runworker(self, conn):
617 def _runworker(self, conn):
617 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
618 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
618 _initworkerprocess()
619 _initworkerprocess()
619 h = self._servicehandler
620 h = self._servicehandler
620 try:
621 try:
621 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
622 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
622 finally:
623 finally:
623 gc.collect() # trigger __del__ since worker process uses os._exit
624 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,242 +1,242
1 #require chg
1 #require chg
2
2
3 $ mkdir log
3 $ mkdir log
4 $ cat <<'EOF' >> $HGRCPATH
4 $ cat <<'EOF' >> $HGRCPATH
5 > [cmdserver]
5 > [cmdserver]
6 > log = $TESTTMP/log/server.log
6 > log = $TESTTMP/log/server.log
7 > max-log-files = 1
7 > max-log-files = 1
8 > max-log-size = 10 kB
8 > max-log-size = 10 kB
9 > EOF
9 > EOF
10 $ cp $HGRCPATH $HGRCPATH.orig
10 $ cp $HGRCPATH $HGRCPATH.orig
11
11
12 $ filterlog () {
12 $ filterlog () {
13 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
13 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
14 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
14 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
15 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
15 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
16 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
16 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
17 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
17 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
18 > }
18 > }
19
19
20 init repo
20 init repo
21
21
22 $ chg init foo
22 $ chg init foo
23 $ cd foo
23 $ cd foo
24
24
25 ill-formed config
25 ill-formed config
26
26
27 $ chg status
27 $ chg status
28 $ echo '=brokenconfig' >> $HGRCPATH
28 $ echo '=brokenconfig' >> $HGRCPATH
29 $ chg status
29 $ chg status
30 hg: parse error at * (glob)
30 hg: parse error at * (glob)
31 [255]
31 [255]
32
32
33 $ cp $HGRCPATH.orig $HGRCPATH
33 $ cp $HGRCPATH.orig $HGRCPATH
34
34
35 long socket path
35 long socket path
36
36
37 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
37 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
38 $ mkdir -p $sockpath
38 $ mkdir -p $sockpath
39 $ bakchgsockname=$CHGSOCKNAME
39 $ bakchgsockname=$CHGSOCKNAME
40 $ CHGSOCKNAME=$sockpath/server
40 $ CHGSOCKNAME=$sockpath/server
41 $ export CHGSOCKNAME
41 $ export CHGSOCKNAME
42 $ chg root
42 $ chg root
43 $TESTTMP/foo
43 $TESTTMP/foo
44 $ rm -rf $sockpath
44 $ rm -rf $sockpath
45 $ CHGSOCKNAME=$bakchgsockname
45 $ CHGSOCKNAME=$bakchgsockname
46 $ export CHGSOCKNAME
46 $ export CHGSOCKNAME
47
47
48 $ cd ..
48 $ cd ..
49
49
50 editor
50 editor
51 ------
51 ------
52
52
53 $ cat >> pushbuffer.py <<EOF
53 $ cat >> pushbuffer.py <<EOF
54 > def reposetup(ui, repo):
54 > def reposetup(ui, repo):
55 > repo.ui.pushbuffer(subproc=True)
55 > repo.ui.pushbuffer(subproc=True)
56 > EOF
56 > EOF
57
57
58 $ chg init editor
58 $ chg init editor
59 $ cd editor
59 $ cd editor
60
60
61 by default, system() should be redirected to the client:
61 by default, system() should be redirected to the client:
62
62
63 $ touch foo
63 $ touch foo
64 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
64 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
65 > | egrep "HG:|run 'cat"
65 > | egrep "HG:|run 'cat"
66 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
66 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
68 HG: Leave message empty to abort commit.
68 HG: Leave message empty to abort commit.
69 HG: --
69 HG: --
70 HG: user: test
70 HG: user: test
71 HG: branch 'default'
71 HG: branch 'default'
72 HG: added foo
72 HG: added foo
73
73
74 but no redirection should be made if output is captured:
74 but no redirection should be made if output is captured:
75
75
76 $ touch bar
76 $ touch bar
77 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
77 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
78 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
78 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
79 > | egrep "HG:|run 'cat"
79 > | egrep "HG:|run 'cat"
80 [1]
80 [1]
81
81
82 check that commit commands succeeded:
82 check that commit commands succeeded:
83
83
84 $ hg log -T '{rev}:{desc}\n'
84 $ hg log -T '{rev}:{desc}\n'
85 1:bufferred
85 1:bufferred
86 0:channeled
86 0:channeled
87
87
88 $ cd ..
88 $ cd ..
89
89
90 pager
90 pager
91 -----
91 -----
92
92
93 $ cat >> fakepager.py <<EOF
93 $ cat >> fakepager.py <<EOF
94 > import sys
94 > import sys
95 > for line in sys.stdin:
95 > for line in sys.stdin:
96 > sys.stdout.write('paged! %r\n' % line)
96 > sys.stdout.write('paged! %r\n' % line)
97 > EOF
97 > EOF
98
98
99 enable pager extension globally, but spawns the master server with no tty:
99 enable pager extension globally, but spawns the master server with no tty:
100
100
101 $ chg init pager
101 $ chg init pager
102 $ cd pager
102 $ cd pager
103 $ cat >> $HGRCPATH <<EOF
103 $ cat >> $HGRCPATH <<EOF
104 > [extensions]
104 > [extensions]
105 > pager =
105 > pager =
106 > [pager]
106 > [pager]
107 > pager = "$PYTHON" $TESTTMP/fakepager.py
107 > pager = "$PYTHON" $TESTTMP/fakepager.py
108 > EOF
108 > EOF
109 $ chg version > /dev/null
109 $ chg version > /dev/null
110 $ touch foo
110 $ touch foo
111 $ chg ci -qAm foo
111 $ chg ci -qAm foo
112
112
113 pager should be enabled if the attached client has a tty:
113 pager should be enabled if the attached client has a tty:
114
114
115 $ chg log -l1 -q --config ui.formatted=True
115 $ chg log -l1 -q --config ui.formatted=True
116 paged! '0:1f7b0de80e11\n'
116 paged! '0:1f7b0de80e11\n'
117 $ chg log -l1 -q --config ui.formatted=False
117 $ chg log -l1 -q --config ui.formatted=False
118 0:1f7b0de80e11
118 0:1f7b0de80e11
119
119
120 chg waits for pager if runcommand raises
120 chg waits for pager if runcommand raises
121
121
122 $ cat > $TESTTMP/crash.py <<EOF
122 $ cat > $TESTTMP/crash.py <<EOF
123 > from mercurial import registrar
123 > from mercurial import registrar
124 > cmdtable = {}
124 > cmdtable = {}
125 > command = registrar.command(cmdtable)
125 > command = registrar.command(cmdtable)
126 > @command(b'crash')
126 > @command(b'crash')
127 > def pagercrash(ui, repo, *pats, **opts):
127 > def pagercrash(ui, repo, *pats, **opts):
128 > ui.write('going to crash\n')
128 > ui.write('going to crash\n')
129 > raise Exception('.')
129 > raise Exception('.')
130 > EOF
130 > EOF
131
131
132 $ cat > $TESTTMP/fakepager.py <<EOF
132 $ cat > $TESTTMP/fakepager.py <<EOF
133 > from __future__ import absolute_import
133 > from __future__ import absolute_import
134 > import sys
134 > import sys
135 > import time
135 > import time
136 > for line in iter(sys.stdin.readline, ''):
136 > for line in iter(sys.stdin.readline, ''):
137 > if 'crash' in line: # only interested in lines containing 'crash'
137 > if 'crash' in line: # only interested in lines containing 'crash'
138 > # if chg exits when pager is sleeping (incorrectly), the output
138 > # if chg exits when pager is sleeping (incorrectly), the output
139 > # will be captured by the next test case
139 > # will be captured by the next test case
140 > time.sleep(1)
140 > time.sleep(1)
141 > sys.stdout.write('crash-pager: %s' % line)
141 > sys.stdout.write('crash-pager: %s' % line)
142 > EOF
142 > EOF
143
143
144 $ cat >> .hg/hgrc <<EOF
144 $ cat >> .hg/hgrc <<EOF
145 > [extensions]
145 > [extensions]
146 > crash = $TESTTMP/crash.py
146 > crash = $TESTTMP/crash.py
147 > EOF
147 > EOF
148
148
149 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
149 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
150 crash-pager: going to crash
150 crash-pager: going to crash
151 [255]
151 [255]
152
152
153 $ cd ..
153 $ cd ..
154
154
155 server lifecycle
155 server lifecycle
156 ----------------
156 ----------------
157
157
158 chg server should be restarted on code change, and old server will shut down
158 chg server should be restarted on code change, and old server will shut down
159 automatically. In this test, we use the following time parameters:
159 automatically. In this test, we use the following time parameters:
160
160
161 - "sleep 1" to make mtime different
161 - "sleep 1" to make mtime different
162 - "sleep 2" to notice mtime change (polling interval is 1 sec)
162 - "sleep 2" to notice mtime change (polling interval is 1 sec)
163
163
164 set up repository with an extension:
164 set up repository with an extension:
165
165
166 $ chg init extreload
166 $ chg init extreload
167 $ cd extreload
167 $ cd extreload
168 $ touch dummyext.py
168 $ touch dummyext.py
169 $ cat <<EOF >> .hg/hgrc
169 $ cat <<EOF >> .hg/hgrc
170 > [extensions]
170 > [extensions]
171 > dummyext = dummyext.py
171 > dummyext = dummyext.py
172 > EOF
172 > EOF
173
173
174 isolate socket directory for stable result:
174 isolate socket directory for stable result:
175
175
176 $ OLDCHGSOCKNAME=$CHGSOCKNAME
176 $ OLDCHGSOCKNAME=$CHGSOCKNAME
177 $ mkdir chgsock
177 $ mkdir chgsock
178 $ CHGSOCKNAME=`pwd`/chgsock/server
178 $ CHGSOCKNAME=`pwd`/chgsock/server
179
179
180 warm up server:
180 warm up server:
181
181
182 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
182 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
183 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
183 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
184
184
185 new server should be started if extension modified:
185 new server should be started if extension modified:
186
186
187 $ sleep 1
187 $ sleep 1
188 $ touch dummyext.py
188 $ touch dummyext.py
189 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
189 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
190 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
190 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
191 chg: debug: * instruction: reconnect (glob)
191 chg: debug: * instruction: reconnect (glob)
192 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
192 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
193
193
194 old server will shut down, while new server should still be reachable:
194 old server will shut down, while new server should still be reachable:
195
195
196 $ sleep 2
196 $ sleep 2
197 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
197 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
198
198
199 socket file should never be unlinked by old server:
199 socket file should never be unlinked by old server:
200 (simulates unowned socket by updating mtime, which makes sure server exits
200 (simulates unowned socket by updating mtime, which makes sure server exits
201 at polling cycle)
201 at polling cycle)
202
202
203 $ ls chgsock/server-*
203 $ ls chgsock/server-*
204 chgsock/server-* (glob)
204 chgsock/server-* (glob)
205 $ touch chgsock/server-*
205 $ touch chgsock/server-*
206 $ sleep 2
206 $ sleep 2
207 $ ls chgsock/server-*
207 $ ls chgsock/server-*
208 chgsock/server-* (glob)
208 chgsock/server-* (glob)
209
209
210 since no server is reachable from socket file, new server should be started:
210 since no server is reachable from socket file, new server should be started:
211 (this test makes sure that old server shut down automatically)
211 (this test makes sure that old server shut down automatically)
212
212
213 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
213 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
214 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
214 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
215
215
216 shut down servers and restore environment:
216 shut down servers and restore environment:
217
217
218 $ rm -R chgsock
218 $ rm -R chgsock
219 $ sleep 2
219 $ sleep 2
220 $ CHGSOCKNAME=$OLDCHGSOCKNAME
220 $ CHGSOCKNAME=$OLDCHGSOCKNAME
221 $ cd ..
221 $ cd ..
222
222
223 check that server events are recorded:
223 check that server events are recorded:
224
224
225 $ ls log
225 $ ls log
226 server.log
226 server.log
227 server.log.1
227 server.log.1
228
228
229 print only the last 10 lines, since we aren't sure how many records are
229 print only the last 10 lines, since we aren't sure how many records are
230 preserved:
230 preserved:
231
231
232 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
232 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
233 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
233 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
234 YYYY/MM/DD HH:MM:SS (PID)> validate: []
235 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
236 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ...
234 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ...
237 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
235 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
238 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
236 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
239 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
237 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
240 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
238 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
241 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
239 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
242 YYYY/MM/DD HH:MM:SS (PID)> validate: []
240 YYYY/MM/DD HH:MM:SS (PID)> validate: []
241 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
242 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
General Comments 0
You need to be logged in to leave comments. Login now