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