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