##// END OF EJS Templates
chgserver: move comments in config example...
Jun Wu -
r30990:cb899ee1 default
parent child Browse files
Show More
@@ -1,588 +1,591 b''
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
22 'setumask' command
23 set umask
23 set umask
24
24
25 'validate' command
25 'validate' command
26 reload the config and check if the server is up to date
26 reload the config and check if the server is up to date
27
27
28 Config
28 Config
29 ------
29 ------
30
30
31 ::
31 ::
32
32
33 [chgserver]
33 [chgserver]
34 idletimeout = 3600 # seconds, after which an idle server will exit
34 # how long (in seconds) should an idle chg server exit
35 skiphash = False # whether to skip config or env change checks
35 idletimeout = 3600
36
37 # whether to skip config or env change checks
38 skiphash = False
36 """
39 """
37
40
38 from __future__ import absolute_import
41 from __future__ import absolute_import
39
42
40 import errno
43 import errno
41 import hashlib
44 import hashlib
42 import inspect
45 import inspect
43 import os
46 import os
44 import re
47 import re
45 import struct
48 import struct
46 import time
49 import time
47
50
48 from .i18n import _
51 from .i18n import _
49
52
50 from . import (
53 from . import (
51 commandserver,
54 commandserver,
52 encoding,
55 encoding,
53 error,
56 error,
54 extensions,
57 extensions,
55 osutil,
58 osutil,
56 pycompat,
59 pycompat,
57 util,
60 util,
58 )
61 )
59
62
60 _log = commandserver.log
63 _log = commandserver.log
61
64
62 def _hashlist(items):
65 def _hashlist(items):
63 """return sha1 hexdigest for a list"""
66 """return sha1 hexdigest for a list"""
64 return hashlib.sha1(str(items)).hexdigest()
67 return hashlib.sha1(str(items)).hexdigest()
65
68
66 # sensitive config sections affecting confighash
69 # sensitive config sections affecting confighash
67 _configsections = [
70 _configsections = [
68 'alias', # affects global state commands.table
71 'alias', # affects global state commands.table
69 'extdiff', # uisetup will register new commands
72 'extdiff', # uisetup will register new commands
70 'extensions',
73 'extensions',
71 ]
74 ]
72
75
73 # sensitive environment variables affecting confighash
76 # sensitive environment variables affecting confighash
74 _envre = re.compile(r'''\A(?:
77 _envre = re.compile(r'''\A(?:
75 CHGHG
78 CHGHG
76 |HG(?:[A-Z].*)?
79 |HG(?:[A-Z].*)?
77 |LANG(?:UAGE)?
80 |LANG(?:UAGE)?
78 |LC_.*
81 |LC_.*
79 |LD_.*
82 |LD_.*
80 |PATH
83 |PATH
81 |PYTHON.*
84 |PYTHON.*
82 |TERM(?:INFO)?
85 |TERM(?:INFO)?
83 |TZ
86 |TZ
84 )\Z''', re.X)
87 )\Z''', re.X)
85
88
86 def _confighash(ui):
89 def _confighash(ui):
87 """return a quick hash for detecting config/env changes
90 """return a quick hash for detecting config/env changes
88
91
89 confighash is the hash of sensitive config items and environment variables.
92 confighash is the hash of sensitive config items and environment variables.
90
93
91 for chgserver, it is designed that once confighash changes, the server is
94 for chgserver, it is designed that once confighash changes, the server is
92 not qualified to serve its client and should redirect the client to a new
95 not qualified to serve its client and should redirect the client to a new
93 server. different from mtimehash, confighash change will not mark the
96 server. different from mtimehash, confighash change will not mark the
94 server outdated and exit since the user can have different configs at the
97 server outdated and exit since the user can have different configs at the
95 same time.
98 same time.
96 """
99 """
97 sectionitems = []
100 sectionitems = []
98 for section in _configsections:
101 for section in _configsections:
99 sectionitems.append(ui.configitems(section))
102 sectionitems.append(ui.configitems(section))
100 sectionhash = _hashlist(sectionitems)
103 sectionhash = _hashlist(sectionitems)
101 envitems = [(k, v) for k, v in encoding.environ.iteritems()
104 envitems = [(k, v) for k, v in encoding.environ.iteritems()
102 if _envre.match(k)]
105 if _envre.match(k)]
103 envhash = _hashlist(sorted(envitems))
106 envhash = _hashlist(sorted(envitems))
104 return sectionhash[:6] + envhash[:6]
107 return sectionhash[:6] + envhash[:6]
105
108
106 def _getmtimepaths(ui):
109 def _getmtimepaths(ui):
107 """get a list of paths that should be checked to detect change
110 """get a list of paths that should be checked to detect change
108
111
109 The list will include:
112 The list will include:
110 - extensions (will not cover all files for complex extensions)
113 - extensions (will not cover all files for complex extensions)
111 - mercurial/__version__.py
114 - mercurial/__version__.py
112 - python binary
115 - python binary
113 """
116 """
114 modules = [m for n, m in extensions.extensions(ui)]
117 modules = [m for n, m in extensions.extensions(ui)]
115 try:
118 try:
116 from . import __version__
119 from . import __version__
117 modules.append(__version__)
120 modules.append(__version__)
118 except ImportError:
121 except ImportError:
119 pass
122 pass
120 files = [pycompat.sysexecutable]
123 files = [pycompat.sysexecutable]
121 for m in modules:
124 for m in modules:
122 try:
125 try:
123 files.append(inspect.getabsfile(m))
126 files.append(inspect.getabsfile(m))
124 except TypeError:
127 except TypeError:
125 pass
128 pass
126 return sorted(set(files))
129 return sorted(set(files))
127
130
128 def _mtimehash(paths):
131 def _mtimehash(paths):
129 """return a quick hash for detecting file changes
132 """return a quick hash for detecting file changes
130
133
131 mtimehash calls stat on given paths and calculate a hash based on size and
134 mtimehash calls stat on given paths and calculate a hash based on size and
132 mtime of each file. mtimehash does not read file content because reading is
135 mtime of each file. mtimehash does not read file content because reading is
133 expensive. therefore it's not 100% reliable for detecting content changes.
136 expensive. therefore it's not 100% reliable for detecting content changes.
134 it's possible to return different hashes for same file contents.
137 it's possible to return different hashes for same file contents.
135 it's also possible to return a same hash for different file contents for
138 it's also possible to return a same hash for different file contents for
136 some carefully crafted situation.
139 some carefully crafted situation.
137
140
138 for chgserver, it is designed that once mtimehash changes, the server is
141 for chgserver, it is designed that once mtimehash changes, the server is
139 considered outdated immediately and should no longer provide service.
142 considered outdated immediately and should no longer provide service.
140
143
141 mtimehash is not included in confighash because we only know the paths of
144 mtimehash is not included in confighash because we only know the paths of
142 extensions after importing them (there is imp.find_module but that faces
145 extensions after importing them (there is imp.find_module but that faces
143 race conditions). We need to calculate confighash without importing.
146 race conditions). We need to calculate confighash without importing.
144 """
147 """
145 def trystat(path):
148 def trystat(path):
146 try:
149 try:
147 st = os.stat(path)
150 st = os.stat(path)
148 return (st.st_mtime, st.st_size)
151 return (st.st_mtime, st.st_size)
149 except OSError:
152 except OSError:
150 # could be ENOENT, EPERM etc. not fatal in any case
153 # could be ENOENT, EPERM etc. not fatal in any case
151 pass
154 pass
152 return _hashlist(map(trystat, paths))[:12]
155 return _hashlist(map(trystat, paths))[:12]
153
156
154 class hashstate(object):
157 class hashstate(object):
155 """a structure storing confighash, mtimehash, paths used for mtimehash"""
158 """a structure storing confighash, mtimehash, paths used for mtimehash"""
156 def __init__(self, confighash, mtimehash, mtimepaths):
159 def __init__(self, confighash, mtimehash, mtimepaths):
157 self.confighash = confighash
160 self.confighash = confighash
158 self.mtimehash = mtimehash
161 self.mtimehash = mtimehash
159 self.mtimepaths = mtimepaths
162 self.mtimepaths = mtimepaths
160
163
161 @staticmethod
164 @staticmethod
162 def fromui(ui, mtimepaths=None):
165 def fromui(ui, mtimepaths=None):
163 if mtimepaths is None:
166 if mtimepaths is None:
164 mtimepaths = _getmtimepaths(ui)
167 mtimepaths = _getmtimepaths(ui)
165 confighash = _confighash(ui)
168 confighash = _confighash(ui)
166 mtimehash = _mtimehash(mtimepaths)
169 mtimehash = _mtimehash(mtimepaths)
167 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
170 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
168 return hashstate(confighash, mtimehash, mtimepaths)
171 return hashstate(confighash, mtimehash, mtimepaths)
169
172
170 def _newchgui(srcui, csystem, attachio):
173 def _newchgui(srcui, csystem, attachio):
171 class chgui(srcui.__class__):
174 class chgui(srcui.__class__):
172 def __init__(self, src=None):
175 def __init__(self, src=None):
173 super(chgui, self).__init__(src)
176 super(chgui, self).__init__(src)
174 if src:
177 if src:
175 self._csystem = getattr(src, '_csystem', csystem)
178 self._csystem = getattr(src, '_csystem', csystem)
176 else:
179 else:
177 self._csystem = csystem
180 self._csystem = csystem
178
181
179 def system(self, cmd, environ=None, cwd=None, onerr=None,
182 def system(self, cmd, environ=None, cwd=None, onerr=None,
180 errprefix=None):
183 errprefix=None):
181 # fallback to the original system method if the output needs to be
184 # fallback to the original system method if the output needs to be
182 # captured (to self._buffers), or the output stream is not stdout
185 # captured (to self._buffers), or the output stream is not stdout
183 # (e.g. stderr, cStringIO), because the chg client is not aware of
186 # (e.g. stderr, cStringIO), because the chg client is not aware of
184 # these situations and will behave differently (write to stdout).
187 # these situations and will behave differently (write to stdout).
185 if (any(s[1] for s in self._bufferstates)
188 if (any(s[1] for s in self._bufferstates)
186 or not util.safehasattr(self.fout, 'fileno')
189 or not util.safehasattr(self.fout, 'fileno')
187 or self.fout.fileno() != util.stdout.fileno()):
190 or self.fout.fileno() != util.stdout.fileno()):
188 return super(chgui, self).system(cmd, environ, cwd, onerr,
191 return super(chgui, self).system(cmd, environ, cwd, onerr,
189 errprefix)
192 errprefix)
190 self.flush()
193 self.flush()
191 rc = self._csystem(cmd, util.shellenviron(environ), cwd)
194 rc = self._csystem(cmd, util.shellenviron(environ), cwd)
192 if rc and onerr:
195 if rc and onerr:
193 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
196 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
194 util.explainexit(rc)[0])
197 util.explainexit(rc)[0])
195 if errprefix:
198 if errprefix:
196 errmsg = '%s: %s' % (errprefix, errmsg)
199 errmsg = '%s: %s' % (errprefix, errmsg)
197 raise onerr(errmsg)
200 raise onerr(errmsg)
198 return rc
201 return rc
199
202
200 def _runpager(self, cmd):
203 def _runpager(self, cmd):
201 self._csystem(cmd, util.shellenviron(), type='pager',
204 self._csystem(cmd, util.shellenviron(), type='pager',
202 cmdtable={'attachio': attachio})
205 cmdtable={'attachio': attachio})
203
206
204 return chgui(srcui)
207 return chgui(srcui)
205
208
206 def _loadnewui(srcui, args):
209 def _loadnewui(srcui, args):
207 from . import dispatch # avoid cycle
210 from . import dispatch # avoid cycle
208
211
209 newui = srcui.__class__.load()
212 newui = srcui.__class__.load()
210 for a in ['fin', 'fout', 'ferr', 'environ']:
213 for a in ['fin', 'fout', 'ferr', 'environ']:
211 setattr(newui, a, getattr(srcui, a))
214 setattr(newui, a, getattr(srcui, a))
212 if util.safehasattr(srcui, '_csystem'):
215 if util.safehasattr(srcui, '_csystem'):
213 newui._csystem = srcui._csystem
216 newui._csystem = srcui._csystem
214
217
215 # command line args
218 # command line args
216 args = args[:]
219 args = args[:]
217 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
220 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
218
221
219 # stolen from tortoisehg.util.copydynamicconfig()
222 # stolen from tortoisehg.util.copydynamicconfig()
220 for section, name, value in srcui.walkconfig():
223 for section, name, value in srcui.walkconfig():
221 source = srcui.configsource(section, name)
224 source = srcui.configsource(section, name)
222 if ':' in source or source == '--config':
225 if ':' in source or source == '--config':
223 # path:line or command line
226 # path:line or command line
224 continue
227 continue
225 newui.setconfig(section, name, value, source)
228 newui.setconfig(section, name, value, source)
226
229
227 # load wd and repo config, copied from dispatch.py
230 # load wd and repo config, copied from dispatch.py
228 cwds = dispatch._earlygetopt(['--cwd'], args)
231 cwds = dispatch._earlygetopt(['--cwd'], args)
229 cwd = cwds and os.path.realpath(cwds[-1]) or None
232 cwd = cwds and os.path.realpath(cwds[-1]) or None
230 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
233 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
231 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
234 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
232
235
233 return (newui, newlui)
236 return (newui, newlui)
234
237
235 class channeledsystem(object):
238 class channeledsystem(object):
236 """Propagate ui.system() request in the following format:
239 """Propagate ui.system() request in the following format:
237
240
238 payload length (unsigned int),
241 payload length (unsigned int),
239 type, '\0',
242 type, '\0',
240 cmd, '\0',
243 cmd, '\0',
241 cwd, '\0',
244 cwd, '\0',
242 envkey, '=', val, '\0',
245 envkey, '=', val, '\0',
243 ...
246 ...
244 envkey, '=', val
247 envkey, '=', val
245
248
246 if type == 'system', waits for:
249 if type == 'system', waits for:
247
250
248 exitcode length (unsigned int),
251 exitcode length (unsigned int),
249 exitcode (int)
252 exitcode (int)
250
253
251 if type == 'pager', repetitively waits for a command name ending with '\n'
254 if type == 'pager', repetitively waits for a command name ending with '\n'
252 and executes it defined by cmdtable, or exits the loop if the command name
255 and executes it defined by cmdtable, or exits the loop if the command name
253 is empty.
256 is empty.
254 """
257 """
255 def __init__(self, in_, out, channel):
258 def __init__(self, in_, out, channel):
256 self.in_ = in_
259 self.in_ = in_
257 self.out = out
260 self.out = out
258 self.channel = channel
261 self.channel = channel
259
262
260 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
263 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
261 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
264 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
262 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
265 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
263 data = '\0'.join(args)
266 data = '\0'.join(args)
264 self.out.write(struct.pack('>cI', self.channel, len(data)))
267 self.out.write(struct.pack('>cI', self.channel, len(data)))
265 self.out.write(data)
268 self.out.write(data)
266 self.out.flush()
269 self.out.flush()
267
270
268 if type == 'system':
271 if type == 'system':
269 length = self.in_.read(4)
272 length = self.in_.read(4)
270 length, = struct.unpack('>I', length)
273 length, = struct.unpack('>I', length)
271 if length != 4:
274 if length != 4:
272 raise error.Abort(_('invalid response'))
275 raise error.Abort(_('invalid response'))
273 rc, = struct.unpack('>i', self.in_.read(4))
276 rc, = struct.unpack('>i', self.in_.read(4))
274 return rc
277 return rc
275 elif type == 'pager':
278 elif type == 'pager':
276 while True:
279 while True:
277 cmd = self.in_.readline()[:-1]
280 cmd = self.in_.readline()[:-1]
278 if not cmd:
281 if not cmd:
279 break
282 break
280 if cmdtable and cmd in cmdtable:
283 if cmdtable and cmd in cmdtable:
281 _log('pager subcommand: %s' % cmd)
284 _log('pager subcommand: %s' % cmd)
282 cmdtable[cmd]()
285 cmdtable[cmd]()
283 else:
286 else:
284 raise error.Abort(_('unexpected command: %s') % cmd)
287 raise error.Abort(_('unexpected command: %s') % cmd)
285 else:
288 else:
286 raise error.ProgrammingError('invalid S channel type: %s' % type)
289 raise error.ProgrammingError('invalid S channel type: %s' % type)
287
290
288 _iochannels = [
291 _iochannels = [
289 # server.ch, ui.fp, mode
292 # server.ch, ui.fp, mode
290 ('cin', 'fin', pycompat.sysstr('rb')),
293 ('cin', 'fin', pycompat.sysstr('rb')),
291 ('cout', 'fout', pycompat.sysstr('wb')),
294 ('cout', 'fout', pycompat.sysstr('wb')),
292 ('cerr', 'ferr', pycompat.sysstr('wb')),
295 ('cerr', 'ferr', pycompat.sysstr('wb')),
293 ]
296 ]
294
297
295 class chgcmdserver(commandserver.server):
298 class chgcmdserver(commandserver.server):
296 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
299 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
297 super(chgcmdserver, self).__init__(
300 super(chgcmdserver, self).__init__(
298 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
301 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
299 repo, fin, fout)
302 repo, fin, fout)
300 self.clientsock = sock
303 self.clientsock = sock
301 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
304 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
302 self.hashstate = hashstate
305 self.hashstate = hashstate
303 self.baseaddress = baseaddress
306 self.baseaddress = baseaddress
304 if hashstate is not None:
307 if hashstate is not None:
305 self.capabilities = self.capabilities.copy()
308 self.capabilities = self.capabilities.copy()
306 self.capabilities['validate'] = chgcmdserver.validate
309 self.capabilities['validate'] = chgcmdserver.validate
307
310
308 def cleanup(self):
311 def cleanup(self):
309 super(chgcmdserver, self).cleanup()
312 super(chgcmdserver, self).cleanup()
310 # dispatch._runcatch() does not flush outputs if exception is not
313 # dispatch._runcatch() does not flush outputs if exception is not
311 # handled by dispatch._dispatch()
314 # handled by dispatch._dispatch()
312 self.ui.flush()
315 self.ui.flush()
313 self._restoreio()
316 self._restoreio()
314
317
315 def attachio(self):
318 def attachio(self):
316 """Attach to client's stdio passed via unix domain socket; all
319 """Attach to client's stdio passed via unix domain socket; all
317 channels except cresult will no longer be used
320 channels except cresult will no longer be used
318 """
321 """
319 # tell client to sendmsg() with 1-byte payload, which makes it
322 # tell client to sendmsg() with 1-byte payload, which makes it
320 # distinctive from "attachio\n" command consumed by client.read()
323 # distinctive from "attachio\n" command consumed by client.read()
321 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
324 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
322 clientfds = osutil.recvfds(self.clientsock.fileno())
325 clientfds = osutil.recvfds(self.clientsock.fileno())
323 _log('received fds: %r\n' % clientfds)
326 _log('received fds: %r\n' % clientfds)
324
327
325 ui = self.ui
328 ui = self.ui
326 ui.flush()
329 ui.flush()
327 first = self._saveio()
330 first = self._saveio()
328 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
331 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
329 assert fd > 0
332 assert fd > 0
330 fp = getattr(ui, fn)
333 fp = getattr(ui, fn)
331 os.dup2(fd, fp.fileno())
334 os.dup2(fd, fp.fileno())
332 os.close(fd)
335 os.close(fd)
333 if not first:
336 if not first:
334 continue
337 continue
335 # reset buffering mode when client is first attached. as we want
338 # reset buffering mode when client is first attached. as we want
336 # to see output immediately on pager, the mode stays unchanged
339 # to see output immediately on pager, the mode stays unchanged
337 # when client re-attached. ferr is unchanged because it should
340 # when client re-attached. ferr is unchanged because it should
338 # be unbuffered no matter if it is a tty or not.
341 # be unbuffered no matter if it is a tty or not.
339 if fn == 'ferr':
342 if fn == 'ferr':
340 newfp = fp
343 newfp = fp
341 else:
344 else:
342 # make it line buffered explicitly because the default is
345 # make it line buffered explicitly because the default is
343 # decided on first write(), where fout could be a pager.
346 # decided on first write(), where fout could be a pager.
344 if fp.isatty():
347 if fp.isatty():
345 bufsize = 1 # line buffered
348 bufsize = 1 # line buffered
346 else:
349 else:
347 bufsize = -1 # system default
350 bufsize = -1 # system default
348 newfp = os.fdopen(fp.fileno(), mode, bufsize)
351 newfp = os.fdopen(fp.fileno(), mode, bufsize)
349 setattr(ui, fn, newfp)
352 setattr(ui, fn, newfp)
350 setattr(self, cn, newfp)
353 setattr(self, cn, newfp)
351
354
352 self.cresult.write(struct.pack('>i', len(clientfds)))
355 self.cresult.write(struct.pack('>i', len(clientfds)))
353
356
354 def _saveio(self):
357 def _saveio(self):
355 if self._oldios:
358 if self._oldios:
356 return False
359 return False
357 ui = self.ui
360 ui = self.ui
358 for cn, fn, _mode in _iochannels:
361 for cn, fn, _mode in _iochannels:
359 ch = getattr(self, cn)
362 ch = getattr(self, cn)
360 fp = getattr(ui, fn)
363 fp = getattr(ui, fn)
361 fd = os.dup(fp.fileno())
364 fd = os.dup(fp.fileno())
362 self._oldios.append((ch, fp, fd))
365 self._oldios.append((ch, fp, fd))
363 return True
366 return True
364
367
365 def _restoreio(self):
368 def _restoreio(self):
366 ui = self.ui
369 ui = self.ui
367 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
370 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
368 newfp = getattr(ui, fn)
371 newfp = getattr(ui, fn)
369 # close newfp while it's associated with client; otherwise it
372 # close newfp while it's associated with client; otherwise it
370 # would be closed when newfp is deleted
373 # would be closed when newfp is deleted
371 if newfp is not fp:
374 if newfp is not fp:
372 newfp.close()
375 newfp.close()
373 # restore original fd: fp is open again
376 # restore original fd: fp is open again
374 os.dup2(fd, fp.fileno())
377 os.dup2(fd, fp.fileno())
375 os.close(fd)
378 os.close(fd)
376 setattr(self, cn, ch)
379 setattr(self, cn, ch)
377 setattr(ui, fn, fp)
380 setattr(ui, fn, fp)
378 del self._oldios[:]
381 del self._oldios[:]
379
382
380 def validate(self):
383 def validate(self):
381 """Reload the config and check if the server is up to date
384 """Reload the config and check if the server is up to date
382
385
383 Read a list of '\0' separated arguments.
386 Read a list of '\0' separated arguments.
384 Write a non-empty list of '\0' separated instruction strings or '\0'
387 Write a non-empty list of '\0' separated instruction strings or '\0'
385 if the list is empty.
388 if the list is empty.
386 An instruction string could be either:
389 An instruction string could be either:
387 - "unlink $path", the client should unlink the path to stop the
390 - "unlink $path", the client should unlink the path to stop the
388 outdated server.
391 outdated server.
389 - "redirect $path", the client should attempt to connect to $path
392 - "redirect $path", the client should attempt to connect to $path
390 first. If it does not work, start a new server. It implies
393 first. If it does not work, start a new server. It implies
391 "reconnect".
394 "reconnect".
392 - "exit $n", the client should exit directly with code n.
395 - "exit $n", the client should exit directly with code n.
393 This may happen if we cannot parse the config.
396 This may happen if we cannot parse the config.
394 - "reconnect", the client should close the connection and
397 - "reconnect", the client should close the connection and
395 reconnect.
398 reconnect.
396 If neither "reconnect" nor "redirect" is included in the instruction
399 If neither "reconnect" nor "redirect" is included in the instruction
397 list, the client can continue with this server after completing all
400 list, the client can continue with this server after completing all
398 the instructions.
401 the instructions.
399 """
402 """
400 from . import dispatch # avoid cycle
403 from . import dispatch # avoid cycle
401
404
402 args = self._readlist()
405 args = self._readlist()
403 try:
406 try:
404 self.ui, lui = _loadnewui(self.ui, args)
407 self.ui, lui = _loadnewui(self.ui, args)
405 except error.ParseError as inst:
408 except error.ParseError as inst:
406 dispatch._formatparse(self.ui.warn, inst)
409 dispatch._formatparse(self.ui.warn, inst)
407 self.ui.flush()
410 self.ui.flush()
408 self.cresult.write('exit 255')
411 self.cresult.write('exit 255')
409 return
412 return
410 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
413 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
411 insts = []
414 insts = []
412 if newhash.mtimehash != self.hashstate.mtimehash:
415 if newhash.mtimehash != self.hashstate.mtimehash:
413 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
416 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
414 insts.append('unlink %s' % addr)
417 insts.append('unlink %s' % addr)
415 # mtimehash is empty if one or more extensions fail to load.
418 # mtimehash is empty if one or more extensions fail to load.
416 # to be compatible with hg, still serve the client this time.
419 # to be compatible with hg, still serve the client this time.
417 if self.hashstate.mtimehash:
420 if self.hashstate.mtimehash:
418 insts.append('reconnect')
421 insts.append('reconnect')
419 if newhash.confighash != self.hashstate.confighash:
422 if newhash.confighash != self.hashstate.confighash:
420 addr = _hashaddress(self.baseaddress, newhash.confighash)
423 addr = _hashaddress(self.baseaddress, newhash.confighash)
421 insts.append('redirect %s' % addr)
424 insts.append('redirect %s' % addr)
422 _log('validate: %s\n' % insts)
425 _log('validate: %s\n' % insts)
423 self.cresult.write('\0'.join(insts) or '\0')
426 self.cresult.write('\0'.join(insts) or '\0')
424
427
425 def chdir(self):
428 def chdir(self):
426 """Change current directory
429 """Change current directory
427
430
428 Note that the behavior of --cwd option is bit different from this.
431 Note that the behavior of --cwd option is bit different from this.
429 It does not affect --config parameter.
432 It does not affect --config parameter.
430 """
433 """
431 path = self._readstr()
434 path = self._readstr()
432 if not path:
435 if not path:
433 return
436 return
434 _log('chdir to %r\n' % path)
437 _log('chdir to %r\n' % path)
435 os.chdir(path)
438 os.chdir(path)
436
439
437 def setumask(self):
440 def setumask(self):
438 """Change umask"""
441 """Change umask"""
439 mask = struct.unpack('>I', self._read(4))[0]
442 mask = struct.unpack('>I', self._read(4))[0]
440 _log('setumask %r\n' % mask)
443 _log('setumask %r\n' % mask)
441 os.umask(mask)
444 os.umask(mask)
442
445
443 def runcommand(self):
446 def runcommand(self):
444 return super(chgcmdserver, self).runcommand()
447 return super(chgcmdserver, self).runcommand()
445
448
446 def setenv(self):
449 def setenv(self):
447 """Clear and update os.environ
450 """Clear and update os.environ
448
451
449 Note that not all variables can make an effect on the running process.
452 Note that not all variables can make an effect on the running process.
450 """
453 """
451 l = self._readlist()
454 l = self._readlist()
452 try:
455 try:
453 newenv = dict(s.split('=', 1) for s in l)
456 newenv = dict(s.split('=', 1) for s in l)
454 except ValueError:
457 except ValueError:
455 raise ValueError('unexpected value in setenv request')
458 raise ValueError('unexpected value in setenv request')
456 _log('setenv: %r\n' % sorted(newenv.keys()))
459 _log('setenv: %r\n' % sorted(newenv.keys()))
457 encoding.environ.clear()
460 encoding.environ.clear()
458 encoding.environ.update(newenv)
461 encoding.environ.update(newenv)
459
462
460 capabilities = commandserver.server.capabilities.copy()
463 capabilities = commandserver.server.capabilities.copy()
461 capabilities.update({'attachio': attachio,
464 capabilities.update({'attachio': attachio,
462 'chdir': chdir,
465 'chdir': chdir,
463 'runcommand': runcommand,
466 'runcommand': runcommand,
464 'setenv': setenv,
467 'setenv': setenv,
465 'setumask': setumask})
468 'setumask': setumask})
466
469
467 if util.safehasattr(osutil, 'setprocname'):
470 if util.safehasattr(osutil, 'setprocname'):
468 def setprocname(self):
471 def setprocname(self):
469 """Change process title"""
472 """Change process title"""
470 name = self._readstr()
473 name = self._readstr()
471 _log('setprocname: %r\n' % name)
474 _log('setprocname: %r\n' % name)
472 osutil.setprocname(name)
475 osutil.setprocname(name)
473 capabilities['setprocname'] = setprocname
476 capabilities['setprocname'] = setprocname
474
477
475 def _tempaddress(address):
478 def _tempaddress(address):
476 return '%s.%d.tmp' % (address, os.getpid())
479 return '%s.%d.tmp' % (address, os.getpid())
477
480
478 def _hashaddress(address, hashstr):
481 def _hashaddress(address, hashstr):
479 # if the basename of address contains '.', use only the left part. this
482 # if the basename of address contains '.', use only the left part. this
480 # makes it possible for the client to pass 'server.tmp$PID' and follow by
483 # makes it possible for the client to pass 'server.tmp$PID' and follow by
481 # an atomic rename to avoid locking when spawning new servers.
484 # an atomic rename to avoid locking when spawning new servers.
482 dirname, basename = os.path.split(address)
485 dirname, basename = os.path.split(address)
483 basename = basename.split('.', 1)[0]
486 basename = basename.split('.', 1)[0]
484 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
487 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
485
488
486 class chgunixservicehandler(object):
489 class chgunixservicehandler(object):
487 """Set of operations for chg services"""
490 """Set of operations for chg services"""
488
491
489 pollinterval = 1 # [sec]
492 pollinterval = 1 # [sec]
490
493
491 def __init__(self, ui):
494 def __init__(self, ui):
492 self.ui = ui
495 self.ui = ui
493 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
496 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
494 self._lastactive = time.time()
497 self._lastactive = time.time()
495
498
496 def bindsocket(self, sock, address):
499 def bindsocket(self, sock, address):
497 self._inithashstate(address)
500 self._inithashstate(address)
498 self._checkextensions()
501 self._checkextensions()
499 self._bind(sock)
502 self._bind(sock)
500 self._createsymlink()
503 self._createsymlink()
501
504
502 def _inithashstate(self, address):
505 def _inithashstate(self, address):
503 self._baseaddress = address
506 self._baseaddress = address
504 if self.ui.configbool('chgserver', 'skiphash', False):
507 if self.ui.configbool('chgserver', 'skiphash', False):
505 self._hashstate = None
508 self._hashstate = None
506 self._realaddress = address
509 self._realaddress = address
507 return
510 return
508 self._hashstate = hashstate.fromui(self.ui)
511 self._hashstate = hashstate.fromui(self.ui)
509 self._realaddress = _hashaddress(address, self._hashstate.confighash)
512 self._realaddress = _hashaddress(address, self._hashstate.confighash)
510
513
511 def _checkextensions(self):
514 def _checkextensions(self):
512 if not self._hashstate:
515 if not self._hashstate:
513 return
516 return
514 if extensions.notloaded():
517 if extensions.notloaded():
515 # one or more extensions failed to load. mtimehash becomes
518 # one or more extensions failed to load. mtimehash becomes
516 # meaningless because we do not know the paths of those extensions.
519 # meaningless because we do not know the paths of those extensions.
517 # set mtimehash to an illegal hash value to invalidate the server.
520 # set mtimehash to an illegal hash value to invalidate the server.
518 self._hashstate.mtimehash = ''
521 self._hashstate.mtimehash = ''
519
522
520 def _bind(self, sock):
523 def _bind(self, sock):
521 # use a unique temp address so we can stat the file and do ownership
524 # use a unique temp address so we can stat the file and do ownership
522 # check later
525 # check later
523 tempaddress = _tempaddress(self._realaddress)
526 tempaddress = _tempaddress(self._realaddress)
524 util.bindunixsocket(sock, tempaddress)
527 util.bindunixsocket(sock, tempaddress)
525 self._socketstat = os.stat(tempaddress)
528 self._socketstat = os.stat(tempaddress)
526 # rename will replace the old socket file if exists atomically. the
529 # rename will replace the old socket file if exists atomically. the
527 # old server will detect ownership change and exit.
530 # old server will detect ownership change and exit.
528 util.rename(tempaddress, self._realaddress)
531 util.rename(tempaddress, self._realaddress)
529
532
530 def _createsymlink(self):
533 def _createsymlink(self):
531 if self._baseaddress == self._realaddress:
534 if self._baseaddress == self._realaddress:
532 return
535 return
533 tempaddress = _tempaddress(self._baseaddress)
536 tempaddress = _tempaddress(self._baseaddress)
534 os.symlink(os.path.basename(self._realaddress), tempaddress)
537 os.symlink(os.path.basename(self._realaddress), tempaddress)
535 util.rename(tempaddress, self._baseaddress)
538 util.rename(tempaddress, self._baseaddress)
536
539
537 def _issocketowner(self):
540 def _issocketowner(self):
538 try:
541 try:
539 stat = os.stat(self._realaddress)
542 stat = os.stat(self._realaddress)
540 return (stat.st_ino == self._socketstat.st_ino and
543 return (stat.st_ino == self._socketstat.st_ino and
541 stat.st_mtime == self._socketstat.st_mtime)
544 stat.st_mtime == self._socketstat.st_mtime)
542 except OSError:
545 except OSError:
543 return False
546 return False
544
547
545 def unlinksocket(self, address):
548 def unlinksocket(self, address):
546 if not self._issocketowner():
549 if not self._issocketowner():
547 return
550 return
548 # it is possible to have a race condition here that we may
551 # it is possible to have a race condition here that we may
549 # remove another server's socket file. but that's okay
552 # remove another server's socket file. but that's okay
550 # since that server will detect and exit automatically and
553 # since that server will detect and exit automatically and
551 # the client will start a new server on demand.
554 # the client will start a new server on demand.
552 try:
555 try:
553 os.unlink(self._realaddress)
556 os.unlink(self._realaddress)
554 except OSError as exc:
557 except OSError as exc:
555 if exc.errno != errno.ENOENT:
558 if exc.errno != errno.ENOENT:
556 raise
559 raise
557
560
558 def printbanner(self, address):
561 def printbanner(self, address):
559 # no "listening at" message should be printed to simulate hg behavior
562 # no "listening at" message should be printed to simulate hg behavior
560 pass
563 pass
561
564
562 def shouldexit(self):
565 def shouldexit(self):
563 if not self._issocketowner():
566 if not self._issocketowner():
564 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
567 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
565 return True
568 return True
566 if time.time() - self._lastactive > self._idletimeout:
569 if time.time() - self._lastactive > self._idletimeout:
567 self.ui.debug('being idle too long. exiting.\n')
570 self.ui.debug('being idle too long. exiting.\n')
568 return True
571 return True
569 return False
572 return False
570
573
571 def newconnection(self):
574 def newconnection(self):
572 self._lastactive = time.time()
575 self._lastactive = time.time()
573
576
574 def createcmdserver(self, repo, conn, fin, fout):
577 def createcmdserver(self, repo, conn, fin, fout):
575 return chgcmdserver(self.ui, repo, fin, fout, conn,
578 return chgcmdserver(self.ui, repo, fin, fout, conn,
576 self._hashstate, self._baseaddress)
579 self._hashstate, self._baseaddress)
577
580
578 def chgunixservice(ui, repo, opts):
581 def chgunixservice(ui, repo, opts):
579 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
582 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
580 # start another chg. drop it to avoid possible side effects.
583 # start another chg. drop it to avoid possible side effects.
581 if 'CHGINTERNALMARK' in encoding.environ:
584 if 'CHGINTERNALMARK' in encoding.environ:
582 del encoding.environ['CHGINTERNALMARK']
585 del encoding.environ['CHGINTERNALMARK']
583
586
584 if repo:
587 if repo:
585 # one chgserver can serve multiple repos. drop repo information
588 # one chgserver can serve multiple repos. drop repo information
586 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
589 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
587 h = chgunixservicehandler(ui)
590 h = chgunixservicehandler(ui)
588 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
591 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now