##// END OF EJS Templates
chgserver: use basename for socket symlink...
Jun Wu -
r28342:bd05d38a default
parent child Browse files
Show More
@@ -1,634 +1,634 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 (EXPERIMENTAL)
8 """command server extension for cHg (EXPERIMENTAL)
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 'getpager' command
19 'getpager' command
20 checks if pager is enabled and which pager should be executed
20 checks if pager is enabled and which pager should be executed
21
21
22 'setenv' command
22 'setenv' command
23 replace os.environ completely
23 replace os.environ completely
24
24
25 'setumask' command
25 'setumask' command
26 set umask
26 set umask
27
27
28 'SIGHUP' signal
28 'SIGHUP' signal
29 reload configuration files
29 reload configuration files
30
30
31 Config
31 Config
32 ------
32 ------
33
33
34 ::
34 ::
35
35
36 [chgserver]
36 [chgserver]
37 idletimeout = 3600 # seconds, after which an idle server will exit
37 idletimeout = 3600 # seconds, after which an idle server will exit
38 skiphash = False # whether to skip config or env change checks
38 skiphash = False # whether to skip config or env change checks
39 """
39 """
40
40
41 from __future__ import absolute_import
41 from __future__ import absolute_import
42
42
43 import SocketServer
43 import SocketServer
44 import errno
44 import errno
45 import inspect
45 import inspect
46 import os
46 import os
47 import re
47 import re
48 import signal
48 import signal
49 import struct
49 import struct
50 import sys
50 import sys
51 import threading
51 import threading
52 import time
52 import time
53 import traceback
53 import traceback
54
54
55 from mercurial.i18n import _
55 from mercurial.i18n import _
56
56
57 from mercurial import (
57 from mercurial import (
58 cmdutil,
58 cmdutil,
59 commands,
59 commands,
60 commandserver,
60 commandserver,
61 dispatch,
61 dispatch,
62 error,
62 error,
63 extensions,
63 extensions,
64 osutil,
64 osutil,
65 util,
65 util,
66 )
66 )
67
67
68 # Note for extension authors: ONLY specify testedwith = 'internal' for
68 # Note for extension authors: ONLY specify testedwith = 'internal' for
69 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
69 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
70 # be specifying the version(s) of Mercurial they are tested with, or
70 # be specifying the version(s) of Mercurial they are tested with, or
71 # leave the attribute unspecified.
71 # leave the attribute unspecified.
72 testedwith = 'internal'
72 testedwith = 'internal'
73
73
74 _log = commandserver.log
74 _log = commandserver.log
75
75
76 def _hashlist(items):
76 def _hashlist(items):
77 """return sha1 hexdigest for a list"""
77 """return sha1 hexdigest for a list"""
78 return util.sha1(str(items)).hexdigest()
78 return util.sha1(str(items)).hexdigest()
79
79
80 # sensitive config sections affecting confighash
80 # sensitive config sections affecting confighash
81 _configsections = ['extensions']
81 _configsections = ['extensions']
82
82
83 # sensitive environment variables affecting confighash
83 # sensitive environment variables affecting confighash
84 _envre = re.compile(r'''\A(?:
84 _envre = re.compile(r'''\A(?:
85 CHGHG
85 CHGHG
86 |HG.*
86 |HG.*
87 |LANG(?:UAGE)?
87 |LANG(?:UAGE)?
88 |LC_.*
88 |LC_.*
89 |LD_.*
89 |LD_.*
90 |PATH
90 |PATH
91 |PYTHON.*
91 |PYTHON.*
92 |TERM(?:INFO)?
92 |TERM(?:INFO)?
93 |TZ
93 |TZ
94 )\Z''', re.X)
94 )\Z''', re.X)
95
95
96 def _confighash(ui):
96 def _confighash(ui):
97 """return a quick hash for detecting config/env changes
97 """return a quick hash for detecting config/env changes
98
98
99 confighash is the hash of sensitive config items and environment variables.
99 confighash is the hash of sensitive config items and environment variables.
100
100
101 for chgserver, it is designed that once confighash changes, the server is
101 for chgserver, it is designed that once confighash changes, the server is
102 not qualified to serve its client and should redirect the client to a new
102 not qualified to serve its client and should redirect the client to a new
103 server. different from mtimehash, confighash change will not mark the
103 server. different from mtimehash, confighash change will not mark the
104 server outdated and exit since the user can have different configs at the
104 server outdated and exit since the user can have different configs at the
105 same time.
105 same time.
106 """
106 """
107 sectionitems = []
107 sectionitems = []
108 for section in _configsections:
108 for section in _configsections:
109 sectionitems.append(ui.configitems(section))
109 sectionitems.append(ui.configitems(section))
110 sectionhash = _hashlist(sectionitems)
110 sectionhash = _hashlist(sectionitems)
111 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
111 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
112 envhash = _hashlist(sorted(envitems))
112 envhash = _hashlist(sorted(envitems))
113 return sectionhash[:6] + envhash[:6]
113 return sectionhash[:6] + envhash[:6]
114
114
115 def _getmtimepaths(ui):
115 def _getmtimepaths(ui):
116 """get a list of paths that should be checked to detect change
116 """get a list of paths that should be checked to detect change
117
117
118 The list will include:
118 The list will include:
119 - extensions (will not cover all files for complex extensions)
119 - extensions (will not cover all files for complex extensions)
120 - mercurial/__version__.py
120 - mercurial/__version__.py
121 - python binary
121 - python binary
122 """
122 """
123 modules = [m for n, m in extensions.extensions(ui)]
123 modules = [m for n, m in extensions.extensions(ui)]
124 try:
124 try:
125 from mercurial import __version__
125 from mercurial import __version__
126 modules.append(__version__)
126 modules.append(__version__)
127 except ImportError:
127 except ImportError:
128 pass
128 pass
129 files = [sys.executable]
129 files = [sys.executable]
130 for m in modules:
130 for m in modules:
131 try:
131 try:
132 files.append(inspect.getabsfile(m))
132 files.append(inspect.getabsfile(m))
133 except TypeError:
133 except TypeError:
134 pass
134 pass
135 return sorted(set(files))
135 return sorted(set(files))
136
136
137 def _mtimehash(paths):
137 def _mtimehash(paths):
138 """return a quick hash for detecting file changes
138 """return a quick hash for detecting file changes
139
139
140 mtimehash calls stat on given paths and calculate a hash based on size and
140 mtimehash calls stat on given paths and calculate a hash based on size and
141 mtime of each file. mtimehash does not read file content because reading is
141 mtime of each file. mtimehash does not read file content because reading is
142 expensive. therefore it's not 100% reliable for detecting content changes.
142 expensive. therefore it's not 100% reliable for detecting content changes.
143 it's possible to return different hashes for same file contents.
143 it's possible to return different hashes for same file contents.
144 it's also possible to return a same hash for different file contents for
144 it's also possible to return a same hash for different file contents for
145 some carefully crafted situation.
145 some carefully crafted situation.
146
146
147 for chgserver, it is designed that once mtimehash changes, the server is
147 for chgserver, it is designed that once mtimehash changes, the server is
148 considered outdated immediately and should no longer provide service.
148 considered outdated immediately and should no longer provide service.
149 """
149 """
150 def trystat(path):
150 def trystat(path):
151 try:
151 try:
152 st = os.stat(path)
152 st = os.stat(path)
153 return (st.st_mtime, st.st_size)
153 return (st.st_mtime, st.st_size)
154 except OSError:
154 except OSError:
155 # could be ENOENT, EPERM etc. not fatal in any case
155 # could be ENOENT, EPERM etc. not fatal in any case
156 pass
156 pass
157 return _hashlist(map(trystat, paths))[:12]
157 return _hashlist(map(trystat, paths))[:12]
158
158
159 class hashstate(object):
159 class hashstate(object):
160 """a structure storing confighash, mtimehash, paths used for mtimehash"""
160 """a structure storing confighash, mtimehash, paths used for mtimehash"""
161 def __init__(self, confighash, mtimehash, mtimepaths):
161 def __init__(self, confighash, mtimehash, mtimepaths):
162 self.confighash = confighash
162 self.confighash = confighash
163 self.mtimehash = mtimehash
163 self.mtimehash = mtimehash
164 self.mtimepaths = mtimepaths
164 self.mtimepaths = mtimepaths
165
165
166 @staticmethod
166 @staticmethod
167 def fromui(ui, mtimepaths=None):
167 def fromui(ui, mtimepaths=None):
168 if mtimepaths is None:
168 if mtimepaths is None:
169 mtimepaths = _getmtimepaths(ui)
169 mtimepaths = _getmtimepaths(ui)
170 confighash = _confighash(ui)
170 confighash = _confighash(ui)
171 mtimehash = _mtimehash(mtimepaths)
171 mtimehash = _mtimehash(mtimepaths)
172 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
172 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
173 return hashstate(confighash, mtimehash, mtimepaths)
173 return hashstate(confighash, mtimehash, mtimepaths)
174
174
175 # copied from hgext/pager.py:uisetup()
175 # copied from hgext/pager.py:uisetup()
176 def _setuppagercmd(ui, options, cmd):
176 def _setuppagercmd(ui, options, cmd):
177 if not ui.formatted():
177 if not ui.formatted():
178 return
178 return
179
179
180 p = ui.config("pager", "pager", os.environ.get("PAGER"))
180 p = ui.config("pager", "pager", os.environ.get("PAGER"))
181 usepager = False
181 usepager = False
182 always = util.parsebool(options['pager'])
182 always = util.parsebool(options['pager'])
183 auto = options['pager'] == 'auto'
183 auto = options['pager'] == 'auto'
184
184
185 if not p:
185 if not p:
186 pass
186 pass
187 elif always:
187 elif always:
188 usepager = True
188 usepager = True
189 elif not auto:
189 elif not auto:
190 usepager = False
190 usepager = False
191 else:
191 else:
192 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
192 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
193 attend = ui.configlist('pager', 'attend', attended)
193 attend = ui.configlist('pager', 'attend', attended)
194 ignore = ui.configlist('pager', 'ignore')
194 ignore = ui.configlist('pager', 'ignore')
195 cmds, _ = cmdutil.findcmd(cmd, commands.table)
195 cmds, _ = cmdutil.findcmd(cmd, commands.table)
196
196
197 for cmd in cmds:
197 for cmd in cmds:
198 var = 'attend-%s' % cmd
198 var = 'attend-%s' % cmd
199 if ui.config('pager', var):
199 if ui.config('pager', var):
200 usepager = ui.configbool('pager', var)
200 usepager = ui.configbool('pager', var)
201 break
201 break
202 if (cmd in attend or
202 if (cmd in attend or
203 (cmd not in ignore and not attend)):
203 (cmd not in ignore and not attend)):
204 usepager = True
204 usepager = True
205 break
205 break
206
206
207 if usepager:
207 if usepager:
208 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
208 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
209 ui.setconfig('ui', 'interactive', False, 'pager')
209 ui.setconfig('ui', 'interactive', False, 'pager')
210 return p
210 return p
211
211
212 _envvarre = re.compile(r'\$[a-zA-Z_]+')
212 _envvarre = re.compile(r'\$[a-zA-Z_]+')
213
213
214 def _clearenvaliases(cmdtable):
214 def _clearenvaliases(cmdtable):
215 """Remove stale command aliases referencing env vars; variable expansion
215 """Remove stale command aliases referencing env vars; variable expansion
216 is done at dispatch.addaliases()"""
216 is done at dispatch.addaliases()"""
217 for name, tab in cmdtable.items():
217 for name, tab in cmdtable.items():
218 cmddef = tab[0]
218 cmddef = tab[0]
219 if (isinstance(cmddef, dispatch.cmdalias) and
219 if (isinstance(cmddef, dispatch.cmdalias) and
220 not cmddef.definition.startswith('!') and # shell alias
220 not cmddef.definition.startswith('!') and # shell alias
221 _envvarre.search(cmddef.definition)):
221 _envvarre.search(cmddef.definition)):
222 del cmdtable[name]
222 del cmdtable[name]
223
223
224 def _newchgui(srcui, csystem):
224 def _newchgui(srcui, csystem):
225 class chgui(srcui.__class__):
225 class chgui(srcui.__class__):
226 def __init__(self, src=None):
226 def __init__(self, src=None):
227 super(chgui, self).__init__(src)
227 super(chgui, self).__init__(src)
228 if src:
228 if src:
229 self._csystem = getattr(src, '_csystem', csystem)
229 self._csystem = getattr(src, '_csystem', csystem)
230 else:
230 else:
231 self._csystem = csystem
231 self._csystem = csystem
232
232
233 def system(self, cmd, environ=None, cwd=None, onerr=None,
233 def system(self, cmd, environ=None, cwd=None, onerr=None,
234 errprefix=None):
234 errprefix=None):
235 # copied from mercurial/util.py:system()
235 # copied from mercurial/util.py:system()
236 self.flush()
236 self.flush()
237 def py2shell(val):
237 def py2shell(val):
238 if val is None or val is False:
238 if val is None or val is False:
239 return '0'
239 return '0'
240 if val is True:
240 if val is True:
241 return '1'
241 return '1'
242 return str(val)
242 return str(val)
243 env = os.environ.copy()
243 env = os.environ.copy()
244 if environ:
244 if environ:
245 env.update((k, py2shell(v)) for k, v in environ.iteritems())
245 env.update((k, py2shell(v)) for k, v in environ.iteritems())
246 env['HG'] = util.hgexecutable()
246 env['HG'] = util.hgexecutable()
247 rc = self._csystem(cmd, env, cwd)
247 rc = self._csystem(cmd, env, cwd)
248 if rc and onerr:
248 if rc and onerr:
249 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
249 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
250 util.explainexit(rc)[0])
250 util.explainexit(rc)[0])
251 if errprefix:
251 if errprefix:
252 errmsg = '%s: %s' % (errprefix, errmsg)
252 errmsg = '%s: %s' % (errprefix, errmsg)
253 raise onerr(errmsg)
253 raise onerr(errmsg)
254 return rc
254 return rc
255
255
256 return chgui(srcui)
256 return chgui(srcui)
257
257
258 def _renewui(srcui, args=None):
258 def _renewui(srcui, args=None):
259 if not args:
259 if not args:
260 args = []
260 args = []
261
261
262 newui = srcui.__class__()
262 newui = srcui.__class__()
263 for a in ['fin', 'fout', 'ferr', 'environ']:
263 for a in ['fin', 'fout', 'ferr', 'environ']:
264 setattr(newui, a, getattr(srcui, a))
264 setattr(newui, a, getattr(srcui, a))
265 if util.safehasattr(srcui, '_csystem'):
265 if util.safehasattr(srcui, '_csystem'):
266 newui._csystem = srcui._csystem
266 newui._csystem = srcui._csystem
267
267
268 # load wd and repo config, copied from dispatch.py
268 # load wd and repo config, copied from dispatch.py
269 cwds = dispatch._earlygetopt(['--cwd'], args)
269 cwds = dispatch._earlygetopt(['--cwd'], args)
270 cwd = cwds and os.path.realpath(cwds[-1]) or None
270 cwd = cwds and os.path.realpath(cwds[-1]) or None
271 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
271 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
272 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
272 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
273
273
274 # internal config: extensions.chgserver
274 # internal config: extensions.chgserver
275 # copy it. it can only be overrided from command line.
275 # copy it. it can only be overrided from command line.
276 newui.setconfig('extensions', 'chgserver',
276 newui.setconfig('extensions', 'chgserver',
277 srcui.config('extensions', 'chgserver'), '--config')
277 srcui.config('extensions', 'chgserver'), '--config')
278
278
279 # command line args
279 # command line args
280 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
280 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
281
281
282 # stolen from tortoisehg.util.copydynamicconfig()
282 # stolen from tortoisehg.util.copydynamicconfig()
283 for section, name, value in srcui.walkconfig():
283 for section, name, value in srcui.walkconfig():
284 source = srcui.configsource(section, name)
284 source = srcui.configsource(section, name)
285 if ':' in source or source == '--config':
285 if ':' in source or source == '--config':
286 # path:line or command line
286 # path:line or command line
287 continue
287 continue
288 if source == 'none':
288 if source == 'none':
289 # ui.configsource returns 'none' by default
289 # ui.configsource returns 'none' by default
290 source = ''
290 source = ''
291 newui.setconfig(section, name, value, source)
291 newui.setconfig(section, name, value, source)
292 return newui
292 return newui
293
293
294 class channeledsystem(object):
294 class channeledsystem(object):
295 """Propagate ui.system() request in the following format:
295 """Propagate ui.system() request in the following format:
296
296
297 payload length (unsigned int),
297 payload length (unsigned int),
298 cmd, '\0',
298 cmd, '\0',
299 cwd, '\0',
299 cwd, '\0',
300 envkey, '=', val, '\0',
300 envkey, '=', val, '\0',
301 ...
301 ...
302 envkey, '=', val
302 envkey, '=', val
303
303
304 and waits:
304 and waits:
305
305
306 exitcode length (unsigned int),
306 exitcode length (unsigned int),
307 exitcode (int)
307 exitcode (int)
308 """
308 """
309 def __init__(self, in_, out, channel):
309 def __init__(self, in_, out, channel):
310 self.in_ = in_
310 self.in_ = in_
311 self.out = out
311 self.out = out
312 self.channel = channel
312 self.channel = channel
313
313
314 def __call__(self, cmd, environ, cwd):
314 def __call__(self, cmd, environ, cwd):
315 args = [util.quotecommand(cmd), cwd or '.']
315 args = [util.quotecommand(cmd), cwd or '.']
316 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
316 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
317 data = '\0'.join(args)
317 data = '\0'.join(args)
318 self.out.write(struct.pack('>cI', self.channel, len(data)))
318 self.out.write(struct.pack('>cI', self.channel, len(data)))
319 self.out.write(data)
319 self.out.write(data)
320 self.out.flush()
320 self.out.flush()
321
321
322 length = self.in_.read(4)
322 length = self.in_.read(4)
323 length, = struct.unpack('>I', length)
323 length, = struct.unpack('>I', length)
324 if length != 4:
324 if length != 4:
325 raise error.Abort(_('invalid response'))
325 raise error.Abort(_('invalid response'))
326 rc, = struct.unpack('>i', self.in_.read(4))
326 rc, = struct.unpack('>i', self.in_.read(4))
327 return rc
327 return rc
328
328
329 _iochannels = [
329 _iochannels = [
330 # server.ch, ui.fp, mode
330 # server.ch, ui.fp, mode
331 ('cin', 'fin', 'rb'),
331 ('cin', 'fin', 'rb'),
332 ('cout', 'fout', 'wb'),
332 ('cout', 'fout', 'wb'),
333 ('cerr', 'ferr', 'wb'),
333 ('cerr', 'ferr', 'wb'),
334 ]
334 ]
335
335
336 class chgcmdserver(commandserver.server):
336 class chgcmdserver(commandserver.server):
337 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
337 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
338 super(chgcmdserver, self).__init__(
338 super(chgcmdserver, self).__init__(
339 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
339 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
340 self.clientsock = sock
340 self.clientsock = sock
341 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
341 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
342 self.hashstate = hashstate
342 self.hashstate = hashstate
343 self.baseaddress = baseaddress
343 self.baseaddress = baseaddress
344
344
345 def cleanup(self):
345 def cleanup(self):
346 # dispatch._runcatch() does not flush outputs if exception is not
346 # dispatch._runcatch() does not flush outputs if exception is not
347 # handled by dispatch._dispatch()
347 # handled by dispatch._dispatch()
348 self.ui.flush()
348 self.ui.flush()
349 self._restoreio()
349 self._restoreio()
350
350
351 def attachio(self):
351 def attachio(self):
352 """Attach to client's stdio passed via unix domain socket; all
352 """Attach to client's stdio passed via unix domain socket; all
353 channels except cresult will no longer be used
353 channels except cresult will no longer be used
354 """
354 """
355 # tell client to sendmsg() with 1-byte payload, which makes it
355 # tell client to sendmsg() with 1-byte payload, which makes it
356 # distinctive from "attachio\n" command consumed by client.read()
356 # distinctive from "attachio\n" command consumed by client.read()
357 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
357 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
358 clientfds = osutil.recvfds(self.clientsock.fileno())
358 clientfds = osutil.recvfds(self.clientsock.fileno())
359 _log('received fds: %r\n' % clientfds)
359 _log('received fds: %r\n' % clientfds)
360
360
361 ui = self.ui
361 ui = self.ui
362 ui.flush()
362 ui.flush()
363 first = self._saveio()
363 first = self._saveio()
364 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
364 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
365 assert fd > 0
365 assert fd > 0
366 fp = getattr(ui, fn)
366 fp = getattr(ui, fn)
367 os.dup2(fd, fp.fileno())
367 os.dup2(fd, fp.fileno())
368 os.close(fd)
368 os.close(fd)
369 if not first:
369 if not first:
370 continue
370 continue
371 # reset buffering mode when client is first attached. as we want
371 # reset buffering mode when client is first attached. as we want
372 # to see output immediately on pager, the mode stays unchanged
372 # to see output immediately on pager, the mode stays unchanged
373 # when client re-attached. ferr is unchanged because it should
373 # when client re-attached. ferr is unchanged because it should
374 # be unbuffered no matter if it is a tty or not.
374 # be unbuffered no matter if it is a tty or not.
375 if fn == 'ferr':
375 if fn == 'ferr':
376 newfp = fp
376 newfp = fp
377 else:
377 else:
378 # make it line buffered explicitly because the default is
378 # make it line buffered explicitly because the default is
379 # decided on first write(), where fout could be a pager.
379 # decided on first write(), where fout could be a pager.
380 if fp.isatty():
380 if fp.isatty():
381 bufsize = 1 # line buffered
381 bufsize = 1 # line buffered
382 else:
382 else:
383 bufsize = -1 # system default
383 bufsize = -1 # system default
384 newfp = os.fdopen(fp.fileno(), mode, bufsize)
384 newfp = os.fdopen(fp.fileno(), mode, bufsize)
385 setattr(ui, fn, newfp)
385 setattr(ui, fn, newfp)
386 setattr(self, cn, newfp)
386 setattr(self, cn, newfp)
387
387
388 self.cresult.write(struct.pack('>i', len(clientfds)))
388 self.cresult.write(struct.pack('>i', len(clientfds)))
389
389
390 def _saveio(self):
390 def _saveio(self):
391 if self._oldios:
391 if self._oldios:
392 return False
392 return False
393 ui = self.ui
393 ui = self.ui
394 for cn, fn, _mode in _iochannels:
394 for cn, fn, _mode in _iochannels:
395 ch = getattr(self, cn)
395 ch = getattr(self, cn)
396 fp = getattr(ui, fn)
396 fp = getattr(ui, fn)
397 fd = os.dup(fp.fileno())
397 fd = os.dup(fp.fileno())
398 self._oldios.append((ch, fp, fd))
398 self._oldios.append((ch, fp, fd))
399 return True
399 return True
400
400
401 def _restoreio(self):
401 def _restoreio(self):
402 ui = self.ui
402 ui = self.ui
403 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
403 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
404 newfp = getattr(ui, fn)
404 newfp = getattr(ui, fn)
405 # close newfp while it's associated with client; otherwise it
405 # close newfp while it's associated with client; otherwise it
406 # would be closed when newfp is deleted
406 # would be closed when newfp is deleted
407 if newfp is not fp:
407 if newfp is not fp:
408 newfp.close()
408 newfp.close()
409 # restore original fd: fp is open again
409 # restore original fd: fp is open again
410 os.dup2(fd, fp.fileno())
410 os.dup2(fd, fp.fileno())
411 os.close(fd)
411 os.close(fd)
412 setattr(self, cn, ch)
412 setattr(self, cn, ch)
413 setattr(ui, fn, fp)
413 setattr(ui, fn, fp)
414 del self._oldios[:]
414 del self._oldios[:]
415
415
416 def chdir(self):
416 def chdir(self):
417 """Change current directory
417 """Change current directory
418
418
419 Note that the behavior of --cwd option is bit different from this.
419 Note that the behavior of --cwd option is bit different from this.
420 It does not affect --config parameter.
420 It does not affect --config parameter.
421 """
421 """
422 path = self._readstr()
422 path = self._readstr()
423 if not path:
423 if not path:
424 return
424 return
425 _log('chdir to %r\n' % path)
425 _log('chdir to %r\n' % path)
426 os.chdir(path)
426 os.chdir(path)
427
427
428 def setumask(self):
428 def setumask(self):
429 """Change umask"""
429 """Change umask"""
430 mask = struct.unpack('>I', self._read(4))[0]
430 mask = struct.unpack('>I', self._read(4))[0]
431 _log('setumask %r\n' % mask)
431 _log('setumask %r\n' % mask)
432 os.umask(mask)
432 os.umask(mask)
433
433
434 def getpager(self):
434 def getpager(self):
435 """Read cmdargs and write pager command to r-channel if enabled
435 """Read cmdargs and write pager command to r-channel if enabled
436
436
437 If pager isn't enabled, this writes '\0' because channeledoutput
437 If pager isn't enabled, this writes '\0' because channeledoutput
438 does not allow to write empty data.
438 does not allow to write empty data.
439 """
439 """
440 args = self._readlist()
440 args = self._readlist()
441 try:
441 try:
442 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
442 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
443 args)
443 args)
444 except (error.Abort, error.AmbiguousCommand, error.CommandError,
444 except (error.Abort, error.AmbiguousCommand, error.CommandError,
445 error.UnknownCommand):
445 error.UnknownCommand):
446 cmd = None
446 cmd = None
447 options = {}
447 options = {}
448 if not cmd or 'pager' not in options:
448 if not cmd or 'pager' not in options:
449 self.cresult.write('\0')
449 self.cresult.write('\0')
450 return
450 return
451
451
452 pagercmd = _setuppagercmd(self.ui, options, cmd)
452 pagercmd = _setuppagercmd(self.ui, options, cmd)
453 if pagercmd:
453 if pagercmd:
454 self.cresult.write(pagercmd)
454 self.cresult.write(pagercmd)
455 else:
455 else:
456 self.cresult.write('\0')
456 self.cresult.write('\0')
457
457
458 def setenv(self):
458 def setenv(self):
459 """Clear and update os.environ
459 """Clear and update os.environ
460
460
461 Note that not all variables can make an effect on the running process.
461 Note that not all variables can make an effect on the running process.
462 """
462 """
463 l = self._readlist()
463 l = self._readlist()
464 try:
464 try:
465 newenv = dict(s.split('=', 1) for s in l)
465 newenv = dict(s.split('=', 1) for s in l)
466 except ValueError:
466 except ValueError:
467 raise ValueError('unexpected value in setenv request')
467 raise ValueError('unexpected value in setenv request')
468
468
469 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
469 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
470 if os.environ.get(k) != newenv.get(k))
470 if os.environ.get(k) != newenv.get(k))
471 _log('change env: %r\n' % sorted(diffkeys))
471 _log('change env: %r\n' % sorted(diffkeys))
472
472
473 os.environ.clear()
473 os.environ.clear()
474 os.environ.update(newenv)
474 os.environ.update(newenv)
475
475
476 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
476 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
477 # reload config so that ui.plain() takes effect
477 # reload config so that ui.plain() takes effect
478 self.ui = _renewui(self.ui)
478 self.ui = _renewui(self.ui)
479
479
480 _clearenvaliases(commands.table)
480 _clearenvaliases(commands.table)
481
481
482 capabilities = commandserver.server.capabilities.copy()
482 capabilities = commandserver.server.capabilities.copy()
483 capabilities.update({'attachio': attachio,
483 capabilities.update({'attachio': attachio,
484 'chdir': chdir,
484 'chdir': chdir,
485 'getpager': getpager,
485 'getpager': getpager,
486 'setenv': setenv,
486 'setenv': setenv,
487 'setumask': setumask})
487 'setumask': setumask})
488
488
489 # copied from mercurial/commandserver.py
489 # copied from mercurial/commandserver.py
490 class _requesthandler(SocketServer.StreamRequestHandler):
490 class _requesthandler(SocketServer.StreamRequestHandler):
491 def handle(self):
491 def handle(self):
492 # use a different process group from the master process, making this
492 # use a different process group from the master process, making this
493 # process pass kernel "is_current_pgrp_orphaned" check so signals like
493 # process pass kernel "is_current_pgrp_orphaned" check so signals like
494 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
494 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
495 os.setpgid(0, 0)
495 os.setpgid(0, 0)
496 ui = self.server.ui
496 ui = self.server.ui
497 repo = self.server.repo
497 repo = self.server.repo
498 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
498 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
499 self.server.hashstate, self.server.baseaddress)
499 self.server.hashstate, self.server.baseaddress)
500 try:
500 try:
501 try:
501 try:
502 sv.serve()
502 sv.serve()
503 # handle exceptions that may be raised by command server. most of
503 # handle exceptions that may be raised by command server. most of
504 # known exceptions are caught by dispatch.
504 # known exceptions are caught by dispatch.
505 except error.Abort as inst:
505 except error.Abort as inst:
506 ui.warn(_('abort: %s\n') % inst)
506 ui.warn(_('abort: %s\n') % inst)
507 except IOError as inst:
507 except IOError as inst:
508 if inst.errno != errno.EPIPE:
508 if inst.errno != errno.EPIPE:
509 raise
509 raise
510 except KeyboardInterrupt:
510 except KeyboardInterrupt:
511 pass
511 pass
512 finally:
512 finally:
513 sv.cleanup()
513 sv.cleanup()
514 except: # re-raises
514 except: # re-raises
515 # also write traceback to error channel. otherwise client cannot
515 # also write traceback to error channel. otherwise client cannot
516 # see it because it is written to server's stderr by default.
516 # see it because it is written to server's stderr by default.
517 traceback.print_exc(file=sv.cerr)
517 traceback.print_exc(file=sv.cerr)
518 raise
518 raise
519
519
520 def _tempaddress(address):
520 def _tempaddress(address):
521 return '%s.%d.tmp' % (address, os.getpid())
521 return '%s.%d.tmp' % (address, os.getpid())
522
522
523 def _hashaddress(address, hashstr):
523 def _hashaddress(address, hashstr):
524 return '%s-%s' % (address, hashstr)
524 return '%s-%s' % (address, hashstr)
525
525
526 class AutoExitMixIn: # use old-style to comply with SocketServer design
526 class AutoExitMixIn: # use old-style to comply with SocketServer design
527 lastactive = time.time()
527 lastactive = time.time()
528 idletimeout = 3600 # default 1 hour
528 idletimeout = 3600 # default 1 hour
529
529
530 def startautoexitthread(self):
530 def startautoexitthread(self):
531 # note: the auto-exit check here is cheap enough to not use a thread,
531 # note: the auto-exit check here is cheap enough to not use a thread,
532 # be done in serve_forever. however SocketServer is hook-unfriendly,
532 # be done in serve_forever. however SocketServer is hook-unfriendly,
533 # you simply cannot hook serve_forever without copying a lot of code.
533 # you simply cannot hook serve_forever without copying a lot of code.
534 # besides, serve_forever's docstring suggests using thread.
534 # besides, serve_forever's docstring suggests using thread.
535 thread = threading.Thread(target=self._autoexitloop)
535 thread = threading.Thread(target=self._autoexitloop)
536 thread.daemon = True
536 thread.daemon = True
537 thread.start()
537 thread.start()
538
538
539 def _autoexitloop(self, interval=1):
539 def _autoexitloop(self, interval=1):
540 while True:
540 while True:
541 time.sleep(interval)
541 time.sleep(interval)
542 if not self.issocketowner():
542 if not self.issocketowner():
543 _log('%s is not owned, exiting.\n' % self.server_address)
543 _log('%s is not owned, exiting.\n' % self.server_address)
544 break
544 break
545 if time.time() - self.lastactive > self.idletimeout:
545 if time.time() - self.lastactive > self.idletimeout:
546 _log('being idle too long. exiting.\n')
546 _log('being idle too long. exiting.\n')
547 break
547 break
548 self.shutdown()
548 self.shutdown()
549
549
550 def process_request(self, request, address):
550 def process_request(self, request, address):
551 self.lastactive = time.time()
551 self.lastactive = time.time()
552 return SocketServer.ForkingMixIn.process_request(
552 return SocketServer.ForkingMixIn.process_request(
553 self, request, address)
553 self, request, address)
554
554
555 def server_bind(self):
555 def server_bind(self):
556 # use a unique temp address so we can stat the file and do ownership
556 # use a unique temp address so we can stat the file and do ownership
557 # check later
557 # check later
558 tempaddress = _tempaddress(self.server_address)
558 tempaddress = _tempaddress(self.server_address)
559 self.socket.bind(tempaddress)
559 self.socket.bind(tempaddress)
560 self._socketstat = os.stat(tempaddress)
560 self._socketstat = os.stat(tempaddress)
561 # rename will replace the old socket file if exists atomically. the
561 # rename will replace the old socket file if exists atomically. the
562 # old server will detect ownership change and exit.
562 # old server will detect ownership change and exit.
563 util.rename(tempaddress, self.server_address)
563 util.rename(tempaddress, self.server_address)
564
564
565 def issocketowner(self):
565 def issocketowner(self):
566 try:
566 try:
567 stat = os.stat(self.server_address)
567 stat = os.stat(self.server_address)
568 return (stat.st_ino == self._socketstat.st_ino and
568 return (stat.st_ino == self._socketstat.st_ino and
569 stat.st_mtime == self._socketstat.st_mtime)
569 stat.st_mtime == self._socketstat.st_mtime)
570 except OSError:
570 except OSError:
571 return False
571 return False
572
572
573 def unlinksocketfile(self):
573 def unlinksocketfile(self):
574 if not self.issocketowner():
574 if not self.issocketowner():
575 return
575 return
576 # it is possible to have a race condition here that we may
576 # it is possible to have a race condition here that we may
577 # remove another server's socket file. but that's okay
577 # remove another server's socket file. but that's okay
578 # since that server will detect and exit automatically and
578 # since that server will detect and exit automatically and
579 # the client will start a new server on demand.
579 # the client will start a new server on demand.
580 try:
580 try:
581 os.unlink(self.server_address)
581 os.unlink(self.server_address)
582 except OSError as exc:
582 except OSError as exc:
583 if exc.errno != errno.ENOENT:
583 if exc.errno != errno.ENOENT:
584 raise
584 raise
585
585
586 class chgunixservice(commandserver.unixservice):
586 class chgunixservice(commandserver.unixservice):
587 def init(self):
587 def init(self):
588 signal.signal(signal.SIGHUP, self._reloadconfig)
588 signal.signal(signal.SIGHUP, self._reloadconfig)
589 self._inithashstate()
589 self._inithashstate()
590 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
590 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
591 SocketServer.UnixStreamServer):
591 SocketServer.UnixStreamServer):
592 ui = self.ui
592 ui = self.ui
593 repo = self.repo
593 repo = self.repo
594 hashstate = self.hashstate
594 hashstate = self.hashstate
595 baseaddress = self.baseaddress
595 baseaddress = self.baseaddress
596 self.server = cls(self.address, _requesthandler)
596 self.server = cls(self.address, _requesthandler)
597 self.server.idletimeout = self.ui.configint(
597 self.server.idletimeout = self.ui.configint(
598 'chgserver', 'idletimeout', self.server.idletimeout)
598 'chgserver', 'idletimeout', self.server.idletimeout)
599 self.server.startautoexitthread()
599 self.server.startautoexitthread()
600 self._createsymlink()
600 self._createsymlink()
601 # avoid writing "listening at" message to stdout before attachio
601 # avoid writing "listening at" message to stdout before attachio
602 # request, which calls setvbuf()
602 # request, which calls setvbuf()
603
603
604 def _inithashstate(self):
604 def _inithashstate(self):
605 self.baseaddress = self.address
605 self.baseaddress = self.address
606 if self.ui.configbool('chgserver', 'skiphash', False):
606 if self.ui.configbool('chgserver', 'skiphash', False):
607 self.hashstate = None
607 self.hashstate = None
608 return
608 return
609 self.hashstate = hashstate.fromui(self.ui)
609 self.hashstate = hashstate.fromui(self.ui)
610 self.address = _hashaddress(self.address, self.hashstate.confighash)
610 self.address = _hashaddress(self.address, self.hashstate.confighash)
611
611
612 def _createsymlink(self):
612 def _createsymlink(self):
613 if self.baseaddress == self.address:
613 if self.baseaddress == self.address:
614 return
614 return
615 tempaddress = _tempaddress(self.baseaddress)
615 tempaddress = _tempaddress(self.baseaddress)
616 os.symlink(self.address, tempaddress)
616 os.symlink(os.path.basename(self.address), tempaddress)
617 util.rename(tempaddress, self.baseaddress)
617 util.rename(tempaddress, self.baseaddress)
618
618
619 def _reloadconfig(self, signum, frame):
619 def _reloadconfig(self, signum, frame):
620 self.ui = self.server.ui = _renewui(self.ui)
620 self.ui = self.server.ui = _renewui(self.ui)
621
621
622 def run(self):
622 def run(self):
623 try:
623 try:
624 self.server.serve_forever()
624 self.server.serve_forever()
625 finally:
625 finally:
626 self.server.unlinksocketfile()
626 self.server.unlinksocketfile()
627
627
628 def uisetup(ui):
628 def uisetup(ui):
629 commandserver._servicemap['chgunix'] = chgunixservice
629 commandserver._servicemap['chgunix'] = chgunixservice
630
630
631 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
631 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
632 # start another chg. drop it to avoid possible side effects.
632 # start another chg. drop it to avoid possible side effects.
633 if 'CHGINTERNALMARK' in os.environ:
633 if 'CHGINTERNALMARK' in os.environ:
634 del os.environ['CHGINTERNALMARK']
634 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now