##// END OF EJS Templates
commandserver: add IPC channel to teach repository path on command finished...
Yuya Nishihara -
r41034:042ed354 default
parent child Browse files
Show More
@@ -1,633 +1,673
1 # commandserver.py - communicate with Mercurial's API over a pipe
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import gc
11 import gc
12 import os
12 import os
13 import random
13 import random
14 import signal
14 import signal
15 import socket
15 import socket
16 import struct
16 import struct
17 import traceback
17 import traceback
18
18
19 try:
19 try:
20 import selectors
20 import selectors
21 selectors.BaseSelector
21 selectors.BaseSelector
22 except ImportError:
22 except ImportError:
23 from .thirdparty import selectors2 as selectors
23 from .thirdparty import selectors2 as selectors
24
24
25 from .i18n import _
25 from .i18n import _
26 from . import (
26 from . import (
27 encoding,
27 encoding,
28 error,
28 error,
29 loggingutil,
29 loggingutil,
30 pycompat,
30 pycompat,
31 util,
31 util,
32 vfs as vfsmod,
32 vfs as vfsmod,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 cborutil,
35 cborutil,
36 procutil,
36 procutil,
37 )
37 )
38
38
39 class channeledoutput(object):
39 class channeledoutput(object):
40 """
40 """
41 Write data to out in the following format:
41 Write data to out in the following format:
42
42
43 data length (unsigned int),
43 data length (unsigned int),
44 data
44 data
45 """
45 """
46 def __init__(self, out, channel):
46 def __init__(self, out, channel):
47 self.out = out
47 self.out = out
48 self.channel = channel
48 self.channel = channel
49
49
50 @property
50 @property
51 def name(self):
51 def name(self):
52 return '<%c-channel>' % self.channel
52 return '<%c-channel>' % self.channel
53
53
54 def write(self, data):
54 def write(self, data):
55 if not data:
55 if not data:
56 return
56 return
57 # single write() to guarantee the same atomicity as the underlying file
57 # single write() to guarantee the same atomicity as the underlying file
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
59 self.out.flush()
59 self.out.flush()
60
60
61 def __getattr__(self, attr):
61 def __getattr__(self, attr):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
63 raise AttributeError(attr)
63 raise AttributeError(attr)
64 return getattr(self.out, attr)
64 return getattr(self.out, attr)
65
65
66 class channeledmessage(object):
66 class channeledmessage(object):
67 """
67 """
68 Write encoded message and metadata to out in the following format:
68 Write encoded message and metadata to out in the following format:
69
69
70 data length (unsigned int),
70 data length (unsigned int),
71 encoded message and metadata, as a flat key-value dict.
71 encoded message and metadata, as a flat key-value dict.
72
72
73 Each message should have 'type' attribute. Messages of unknown type
73 Each message should have 'type' attribute. Messages of unknown type
74 should be ignored.
74 should be ignored.
75 """
75 """
76
76
77 # teach ui that write() can take **opts
77 # teach ui that write() can take **opts
78 structured = True
78 structured = True
79
79
80 def __init__(self, out, channel, encodename, encodefn):
80 def __init__(self, out, channel, encodename, encodefn):
81 self._cout = channeledoutput(out, channel)
81 self._cout = channeledoutput(out, channel)
82 self.encoding = encodename
82 self.encoding = encodename
83 self._encodefn = encodefn
83 self._encodefn = encodefn
84
84
85 def write(self, data, **opts):
85 def write(self, data, **opts):
86 opts = pycompat.byteskwargs(opts)
86 opts = pycompat.byteskwargs(opts)
87 if data is not None:
87 if data is not None:
88 opts[b'data'] = data
88 opts[b'data'] = data
89 self._cout.write(self._encodefn(opts))
89 self._cout.write(self._encodefn(opts))
90
90
91 def __getattr__(self, attr):
91 def __getattr__(self, attr):
92 return getattr(self._cout, attr)
92 return getattr(self._cout, attr)
93
93
94 class channeledinput(object):
94 class channeledinput(object):
95 """
95 """
96 Read data from in_.
96 Read data from in_.
97
97
98 Requests for input are written to out in the following format:
98 Requests for input are written to out in the following format:
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
100 how many bytes to send at most (unsigned int),
100 how many bytes to send at most (unsigned int),
101
101
102 The client replies with:
102 The client replies with:
103 data length (unsigned int), 0 meaning EOF
103 data length (unsigned int), 0 meaning EOF
104 data
104 data
105 """
105 """
106
106
107 maxchunksize = 4 * 1024
107 maxchunksize = 4 * 1024
108
108
109 def __init__(self, in_, out, channel):
109 def __init__(self, in_, out, channel):
110 self.in_ = in_
110 self.in_ = in_
111 self.out = out
111 self.out = out
112 self.channel = channel
112 self.channel = channel
113
113
114 @property
114 @property
115 def name(self):
115 def name(self):
116 return '<%c-channel>' % self.channel
116 return '<%c-channel>' % self.channel
117
117
118 def read(self, size=-1):
118 def read(self, size=-1):
119 if size < 0:
119 if size < 0:
120 # if we need to consume all the clients input, ask for 4k chunks
120 # if we need to consume all the clients input, ask for 4k chunks
121 # so the pipe doesn't fill up risking a deadlock
121 # so the pipe doesn't fill up risking a deadlock
122 size = self.maxchunksize
122 size = self.maxchunksize
123 s = self._read(size, self.channel)
123 s = self._read(size, self.channel)
124 buf = s
124 buf = s
125 while s:
125 while s:
126 s = self._read(size, self.channel)
126 s = self._read(size, self.channel)
127 buf += s
127 buf += s
128
128
129 return buf
129 return buf
130 else:
130 else:
131 return self._read(size, self.channel)
131 return self._read(size, self.channel)
132
132
133 def _read(self, size, channel):
133 def _read(self, size, channel):
134 if not size:
134 if not size:
135 return ''
135 return ''
136 assert size > 0
136 assert size > 0
137
137
138 # tell the client we need at most size bytes
138 # tell the client we need at most size bytes
139 self.out.write(struct.pack('>cI', channel, size))
139 self.out.write(struct.pack('>cI', channel, size))
140 self.out.flush()
140 self.out.flush()
141
141
142 length = self.in_.read(4)
142 length = self.in_.read(4)
143 length = struct.unpack('>I', length)[0]
143 length = struct.unpack('>I', length)[0]
144 if not length:
144 if not length:
145 return ''
145 return ''
146 else:
146 else:
147 return self.in_.read(length)
147 return self.in_.read(length)
148
148
149 def readline(self, size=-1):
149 def readline(self, size=-1):
150 if size < 0:
150 if size < 0:
151 size = self.maxchunksize
151 size = self.maxchunksize
152 s = self._read(size, 'L')
152 s = self._read(size, 'L')
153 buf = s
153 buf = s
154 # keep asking for more until there's either no more or
154 # keep asking for more until there's either no more or
155 # we got a full line
155 # we got a full line
156 while s and s[-1] != '\n':
156 while s and s[-1] != '\n':
157 s = self._read(size, 'L')
157 s = self._read(size, 'L')
158 buf += s
158 buf += s
159
159
160 return buf
160 return buf
161 else:
161 else:
162 return self._read(size, 'L')
162 return self._read(size, 'L')
163
163
164 def __iter__(self):
164 def __iter__(self):
165 return self
165 return self
166
166
167 def next(self):
167 def next(self):
168 l = self.readline()
168 l = self.readline()
169 if not l:
169 if not l:
170 raise StopIteration
170 raise StopIteration
171 return l
171 return l
172
172
173 __next__ = next
173 __next__ = next
174
174
175 def __getattr__(self, attr):
175 def __getattr__(self, attr):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
177 raise AttributeError(attr)
177 raise AttributeError(attr)
178 return getattr(self.in_, attr)
178 return getattr(self.in_, attr)
179
179
180 _messageencoders = {
180 _messageencoders = {
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
182 }
182 }
183
183
184 def _selectmessageencoder(ui):
184 def _selectmessageencoder(ui):
185 # experimental config: cmdserver.message-encodings
185 # experimental config: cmdserver.message-encodings
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
187 for n in encnames:
187 for n in encnames:
188 f = _messageencoders.get(n)
188 f = _messageencoders.get(n)
189 if f:
189 if f:
190 return n, f
190 return n, f
191 raise error.Abort(b'no supported message encodings: %s'
191 raise error.Abort(b'no supported message encodings: %s'
192 % b' '.join(encnames))
192 % b' '.join(encnames))
193
193
194 class server(object):
194 class server(object):
195 """
195 """
196 Listens for commands on fin, runs them and writes the output on a channel
196 Listens for commands on fin, runs them and writes the output on a channel
197 based stream to fout.
197 based stream to fout.
198 """
198 """
199 def __init__(self, ui, repo, fin, fout, prereposetups=None):
199 def __init__(self, ui, repo, fin, fout, prereposetups=None):
200 self.cwd = encoding.getcwd()
200 self.cwd = encoding.getcwd()
201
201
202 if repo:
202 if repo:
203 # the ui here is really the repo ui so take its baseui so we don't
203 # the ui here is really the repo ui so take its baseui so we don't
204 # end up with its local configuration
204 # end up with its local configuration
205 self.ui = repo.baseui
205 self.ui = repo.baseui
206 self.repo = repo
206 self.repo = repo
207 self.repoui = repo.ui
207 self.repoui = repo.ui
208 else:
208 else:
209 self.ui = ui
209 self.ui = ui
210 self.repo = self.repoui = None
210 self.repo = self.repoui = None
211 self._prereposetups = prereposetups
211 self._prereposetups = prereposetups
212
212
213 self.cdebug = channeledoutput(fout, 'd')
213 self.cdebug = channeledoutput(fout, 'd')
214 self.cerr = channeledoutput(fout, 'e')
214 self.cerr = channeledoutput(fout, 'e')
215 self.cout = channeledoutput(fout, 'o')
215 self.cout = channeledoutput(fout, 'o')
216 self.cin = channeledinput(fin, fout, 'I')
216 self.cin = channeledinput(fin, fout, 'I')
217 self.cresult = channeledoutput(fout, 'r')
217 self.cresult = channeledoutput(fout, 'r')
218
218
219 if self.ui.config(b'cmdserver', b'log') == b'-':
219 if self.ui.config(b'cmdserver', b'log') == b'-':
220 # switch log stream of server's ui to the 'd' (debug) channel
220 # switch log stream of server's ui to the 'd' (debug) channel
221 # (don't touch repo.ui as its lifetime is longer than the server)
221 # (don't touch repo.ui as its lifetime is longer than the server)
222 self.ui = self.ui.copy()
222 self.ui = self.ui.copy()
223 setuplogging(self.ui, repo=None, fp=self.cdebug)
223 setuplogging(self.ui, repo=None, fp=self.cdebug)
224
224
225 # TODO: add this to help/config.txt when stabilized
225 # TODO: add this to help/config.txt when stabilized
226 # ``channel``
226 # ``channel``
227 # Use separate channel for structured output. (Command-server only)
227 # Use separate channel for structured output. (Command-server only)
228 self.cmsg = None
228 self.cmsg = None
229 if ui.config(b'ui', b'message-output') == b'channel':
229 if ui.config(b'ui', b'message-output') == b'channel':
230 encname, encfn = _selectmessageencoder(ui)
230 encname, encfn = _selectmessageencoder(ui)
231 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
231 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
232
232
233 self.client = fin
233 self.client = fin
234
234
235 def cleanup(self):
235 def cleanup(self):
236 """release and restore resources taken during server session"""
236 """release and restore resources taken during server session"""
237
237
238 def _read(self, size):
238 def _read(self, size):
239 if not size:
239 if not size:
240 return ''
240 return ''
241
241
242 data = self.client.read(size)
242 data = self.client.read(size)
243
243
244 # is the other end closed?
244 # is the other end closed?
245 if not data:
245 if not data:
246 raise EOFError
246 raise EOFError
247
247
248 return data
248 return data
249
249
250 def _readstr(self):
250 def _readstr(self):
251 """read a string from the channel
251 """read a string from the channel
252
252
253 format:
253 format:
254 data length (uint32), data
254 data length (uint32), data
255 """
255 """
256 length = struct.unpack('>I', self._read(4))[0]
256 length = struct.unpack('>I', self._read(4))[0]
257 if not length:
257 if not length:
258 return ''
258 return ''
259 return self._read(length)
259 return self._read(length)
260
260
261 def _readlist(self):
261 def _readlist(self):
262 """read a list of NULL separated strings from the channel"""
262 """read a list of NULL separated strings from the channel"""
263 s = self._readstr()
263 s = self._readstr()
264 if s:
264 if s:
265 return s.split('\0')
265 return s.split('\0')
266 else:
266 else:
267 return []
267 return []
268
268
269 def runcommand(self):
269 def runcommand(self):
270 """ reads a list of \0 terminated arguments, executes
270 """ reads a list of \0 terminated arguments, executes
271 and writes the return code to the result channel """
271 and writes the return code to the result channel """
272 from . import dispatch # avoid cycle
272 from . import dispatch # avoid cycle
273
273
274 args = self._readlist()
274 args = self._readlist()
275
275
276 # copy the uis so changes (e.g. --config or --verbose) don't
276 # copy the uis so changes (e.g. --config or --verbose) don't
277 # persist between requests
277 # persist between requests
278 copiedui = self.ui.copy()
278 copiedui = self.ui.copy()
279 uis = [copiedui]
279 uis = [copiedui]
280 if self.repo:
280 if self.repo:
281 self.repo.baseui = copiedui
281 self.repo.baseui = copiedui
282 # clone ui without using ui.copy because this is protected
282 # clone ui without using ui.copy because this is protected
283 repoui = self.repoui.__class__(self.repoui)
283 repoui = self.repoui.__class__(self.repoui)
284 repoui.copy = copiedui.copy # redo copy protection
284 repoui.copy = copiedui.copy # redo copy protection
285 uis.append(repoui)
285 uis.append(repoui)
286 self.repo.ui = self.repo.dirstate._ui = repoui
286 self.repo.ui = self.repo.dirstate._ui = repoui
287 self.repo.invalidateall()
287 self.repo.invalidateall()
288
288
289 for ui in uis:
289 for ui in uis:
290 ui.resetstate()
290 ui.resetstate()
291 # any kind of interaction must use server channels, but chg may
291 # any kind of interaction must use server channels, but chg may
292 # replace channels by fully functional tty files. so nontty is
292 # replace channels by fully functional tty files. so nontty is
293 # enforced only if cin is a channel.
293 # enforced only if cin is a channel.
294 if not util.safehasattr(self.cin, 'fileno'):
294 if not util.safehasattr(self.cin, 'fileno'):
295 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
295 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
296
296
297 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
297 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
298 self.cout, self.cerr, self.cmsg,
298 self.cout, self.cerr, self.cmsg,
299 prereposetups=self._prereposetups)
299 prereposetups=self._prereposetups)
300
300
301 try:
301 try:
302 ret = dispatch.dispatch(req) & 255
302 ret = dispatch.dispatch(req) & 255
303 self.cresult.write(struct.pack('>i', int(ret)))
303 self.cresult.write(struct.pack('>i', int(ret)))
304 finally:
304 finally:
305 # restore old cwd
305 # restore old cwd
306 if '--cwd' in args:
306 if '--cwd' in args:
307 os.chdir(self.cwd)
307 os.chdir(self.cwd)
308
308
309 def getencoding(self):
309 def getencoding(self):
310 """ writes the current encoding to the result channel """
310 """ writes the current encoding to the result channel """
311 self.cresult.write(encoding.encoding)
311 self.cresult.write(encoding.encoding)
312
312
313 def serveone(self):
313 def serveone(self):
314 cmd = self.client.readline()[:-1]
314 cmd = self.client.readline()[:-1]
315 if cmd:
315 if cmd:
316 handler = self.capabilities.get(cmd)
316 handler = self.capabilities.get(cmd)
317 if handler:
317 if handler:
318 handler(self)
318 handler(self)
319 else:
319 else:
320 # clients are expected to check what commands are supported by
320 # clients are expected to check what commands are supported by
321 # looking at the servers capabilities
321 # looking at the servers capabilities
322 raise error.Abort(_('unknown command %s') % cmd)
322 raise error.Abort(_('unknown command %s') % cmd)
323
323
324 return cmd != ''
324 return cmd != ''
325
325
326 capabilities = {'runcommand': runcommand,
326 capabilities = {'runcommand': runcommand,
327 'getencoding': getencoding}
327 'getencoding': getencoding}
328
328
329 def serve(self):
329 def serve(self):
330 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
330 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
331 hellomsg += '\n'
331 hellomsg += '\n'
332 hellomsg += 'encoding: ' + encoding.encoding
332 hellomsg += 'encoding: ' + encoding.encoding
333 hellomsg += '\n'
333 hellomsg += '\n'
334 if self.cmsg:
334 if self.cmsg:
335 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
335 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
336 hellomsg += 'pid: %d' % procutil.getpid()
336 hellomsg += 'pid: %d' % procutil.getpid()
337 if util.safehasattr(os, 'getpgid'):
337 if util.safehasattr(os, 'getpgid'):
338 hellomsg += '\n'
338 hellomsg += '\n'
339 hellomsg += 'pgid: %d' % os.getpgid(0)
339 hellomsg += 'pgid: %d' % os.getpgid(0)
340
340
341 # write the hello msg in -one- chunk
341 # write the hello msg in -one- chunk
342 self.cout.write(hellomsg)
342 self.cout.write(hellomsg)
343
343
344 try:
344 try:
345 while self.serveone():
345 while self.serveone():
346 pass
346 pass
347 except EOFError:
347 except EOFError:
348 # we'll get here if the client disconnected while we were reading
348 # we'll get here if the client disconnected while we were reading
349 # its request
349 # its request
350 return 1
350 return 1
351
351
352 return 0
352 return 0
353
353
354 def setuplogging(ui, repo=None, fp=None):
354 def setuplogging(ui, repo=None, fp=None):
355 """Set up server logging facility
355 """Set up server logging facility
356
356
357 If cmdserver.log is '-', log messages will be sent to the given fp.
357 If cmdserver.log is '-', log messages will be sent to the given fp.
358 It should be the 'd' channel while a client is connected, and otherwise
358 It should be the 'd' channel while a client is connected, and otherwise
359 is the stderr of the server process.
359 is the stderr of the server process.
360 """
360 """
361 # developer config: cmdserver.log
361 # developer config: cmdserver.log
362 logpath = ui.config(b'cmdserver', b'log')
362 logpath = ui.config(b'cmdserver', b'log')
363 if not logpath:
363 if not logpath:
364 return
364 return
365 # developer config: cmdserver.track-log
365 # developer config: cmdserver.track-log
366 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
366 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
367
367
368 if logpath == b'-' and fp:
368 if logpath == b'-' and fp:
369 logger = loggingutil.fileobjectlogger(fp, tracked)
369 logger = loggingutil.fileobjectlogger(fp, tracked)
370 elif logpath == b'-':
370 elif logpath == b'-':
371 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
371 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
372 else:
372 else:
373 logpath = os.path.abspath(util.expandpath(logpath))
373 logpath = os.path.abspath(util.expandpath(logpath))
374 # developer config: cmdserver.max-log-files
374 # developer config: cmdserver.max-log-files
375 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
375 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
376 # developer config: cmdserver.max-log-size
376 # developer config: cmdserver.max-log-size
377 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
377 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
378 vfs = vfsmod.vfs(os.path.dirname(logpath))
378 vfs = vfsmod.vfs(os.path.dirname(logpath))
379 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
379 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
380 maxfiles=maxfiles, maxsize=maxsize)
380 maxfiles=maxfiles, maxsize=maxsize)
381
381
382 targetuis = {ui}
382 targetuis = {ui}
383 if repo:
383 if repo:
384 targetuis.add(repo.baseui)
384 targetuis.add(repo.baseui)
385 targetuis.add(repo.ui)
385 targetuis.add(repo.ui)
386 for u in targetuis:
386 for u in targetuis:
387 u.setlogger(b'cmdserver', logger)
387 u.setlogger(b'cmdserver', logger)
388
388
389 class pipeservice(object):
389 class pipeservice(object):
390 def __init__(self, ui, repo, opts):
390 def __init__(self, ui, repo, opts):
391 self.ui = ui
391 self.ui = ui
392 self.repo = repo
392 self.repo = repo
393
393
394 def init(self):
394 def init(self):
395 pass
395 pass
396
396
397 def run(self):
397 def run(self):
398 ui = self.ui
398 ui = self.ui
399 # redirect stdio to null device so that broken extensions or in-process
399 # redirect stdio to null device so that broken extensions or in-process
400 # hooks will never cause corruption of channel protocol.
400 # hooks will never cause corruption of channel protocol.
401 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
401 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
402 sv = server(ui, self.repo, fin, fout)
402 sv = server(ui, self.repo, fin, fout)
403 try:
403 try:
404 return sv.serve()
404 return sv.serve()
405 finally:
405 finally:
406 sv.cleanup()
406 sv.cleanup()
407
407
408 def _initworkerprocess():
408 def _initworkerprocess():
409 # use a different process group from the master process, in order to:
409 # use a different process group from the master process, in order to:
410 # 1. make the current process group no longer "orphaned" (because the
410 # 1. make the current process group no longer "orphaned" (because the
411 # parent of this process is in a different process group while
411 # parent of this process is in a different process group while
412 # remains in a same session)
412 # remains in a same session)
413 # according to POSIX 2.2.2.52, orphaned process group will ignore
413 # according to POSIX 2.2.2.52, orphaned process group will ignore
414 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
414 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
415 # cause trouble for things like ncurses.
415 # cause trouble for things like ncurses.
416 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
416 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
417 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
417 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
418 # processes like ssh will be killed properly, without affecting
418 # processes like ssh will be killed properly, without affecting
419 # unrelated processes.
419 # unrelated processes.
420 os.setpgid(0, 0)
420 os.setpgid(0, 0)
421 # change random state otherwise forked request handlers would have a
421 # change random state otherwise forked request handlers would have a
422 # same state inherited from parent.
422 # same state inherited from parent.
423 random.seed()
423 random.seed()
424
424
425 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
425 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
426 fin = conn.makefile(r'rb')
426 fin = conn.makefile(r'rb')
427 fout = conn.makefile(r'wb')
427 fout = conn.makefile(r'wb')
428 sv = None
428 sv = None
429 try:
429 try:
430 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
430 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
431 try:
431 try:
432 sv.serve()
432 sv.serve()
433 # handle exceptions that may be raised by command server. most of
433 # handle exceptions that may be raised by command server. most of
434 # known exceptions are caught by dispatch.
434 # known exceptions are caught by dispatch.
435 except error.Abort as inst:
435 except error.Abort as inst:
436 ui.error(_('abort: %s\n') % inst)
436 ui.error(_('abort: %s\n') % inst)
437 except IOError as inst:
437 except IOError as inst:
438 if inst.errno != errno.EPIPE:
438 if inst.errno != errno.EPIPE:
439 raise
439 raise
440 except KeyboardInterrupt:
440 except KeyboardInterrupt:
441 pass
441 pass
442 finally:
442 finally:
443 sv.cleanup()
443 sv.cleanup()
444 except: # re-raises
444 except: # re-raises
445 # also write traceback to error channel. otherwise client cannot
445 # also write traceback to error channel. otherwise client cannot
446 # see it because it is written to server's stderr by default.
446 # see it because it is written to server's stderr by default.
447 if sv:
447 if sv:
448 cerr = sv.cerr
448 cerr = sv.cerr
449 else:
449 else:
450 cerr = channeledoutput(fout, 'e')
450 cerr = channeledoutput(fout, 'e')
451 cerr.write(encoding.strtolocal(traceback.format_exc()))
451 cerr.write(encoding.strtolocal(traceback.format_exc()))
452 raise
452 raise
453 finally:
453 finally:
454 fin.close()
454 fin.close()
455 try:
455 try:
456 fout.close() # implicit flush() may cause another EPIPE
456 fout.close() # implicit flush() may cause another EPIPE
457 except IOError as inst:
457 except IOError as inst:
458 if inst.errno != errno.EPIPE:
458 if inst.errno != errno.EPIPE:
459 raise
459 raise
460
460
461 class unixservicehandler(object):
461 class unixservicehandler(object):
462 """Set of pluggable operations for unix-mode services
462 """Set of pluggable operations for unix-mode services
463
463
464 Almost all methods except for createcmdserver() are called in the main
464 Almost all methods except for createcmdserver() are called in the main
465 process. You can't pass mutable resource back from createcmdserver().
465 process. You can't pass mutable resource back from createcmdserver().
466 """
466 """
467
467
468 pollinterval = None
468 pollinterval = None
469
469
470 def __init__(self, ui):
470 def __init__(self, ui):
471 self.ui = ui
471 self.ui = ui
472
472
473 def bindsocket(self, sock, address):
473 def bindsocket(self, sock, address):
474 util.bindunixsocket(sock, address)
474 util.bindunixsocket(sock, address)
475 sock.listen(socket.SOMAXCONN)
475 sock.listen(socket.SOMAXCONN)
476 self.ui.status(_('listening at %s\n') % address)
476 self.ui.status(_('listening at %s\n') % address)
477 self.ui.flush() # avoid buffering of status message
477 self.ui.flush() # avoid buffering of status message
478
478
479 def unlinksocket(self, address):
479 def unlinksocket(self, address):
480 os.unlink(address)
480 os.unlink(address)
481
481
482 def shouldexit(self):
482 def shouldexit(self):
483 """True if server should shut down; checked per pollinterval"""
483 """True if server should shut down; checked per pollinterval"""
484 return False
484 return False
485
485
486 def newconnection(self):
486 def newconnection(self):
487 """Called when main process notices new connection"""
487 """Called when main process notices new connection"""
488
488
489 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
489 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
490 """Create new command server instance; called in the process that
490 """Create new command server instance; called in the process that
491 serves for the current connection"""
491 serves for the current connection"""
492 return server(self.ui, repo, fin, fout, prereposetups)
492 return server(self.ui, repo, fin, fout, prereposetups)
493
493
494 class unixforkingservice(object):
494 class unixforkingservice(object):
495 """
495 """
496 Listens on unix domain socket and forks server per connection
496 Listens on unix domain socket and forks server per connection
497 """
497 """
498
498
499 def __init__(self, ui, repo, opts, handler=None):
499 def __init__(self, ui, repo, opts, handler=None):
500 self.ui = ui
500 self.ui = ui
501 self.repo = repo
501 self.repo = repo
502 self.address = opts['address']
502 self.address = opts['address']
503 if not util.safehasattr(socket, 'AF_UNIX'):
503 if not util.safehasattr(socket, 'AF_UNIX'):
504 raise error.Abort(_('unsupported platform'))
504 raise error.Abort(_('unsupported platform'))
505 if not self.address:
505 if not self.address:
506 raise error.Abort(_('no socket path specified with --address'))
506 raise error.Abort(_('no socket path specified with --address'))
507 self._servicehandler = handler or unixservicehandler(ui)
507 self._servicehandler = handler or unixservicehandler(ui)
508 self._sock = None
508 self._sock = None
509 self._mainipc = None
510 self._workeripc = None
509 self._oldsigchldhandler = None
511 self._oldsigchldhandler = None
510 self._workerpids = set() # updated by signal handler; do not iterate
512 self._workerpids = set() # updated by signal handler; do not iterate
511 self._socketunlinked = None
513 self._socketunlinked = None
512
514
513 def init(self):
515 def init(self):
514 self._sock = socket.socket(socket.AF_UNIX)
516 self._sock = socket.socket(socket.AF_UNIX)
517 # IPC channel from many workers to one main process; this is actually
518 # a uni-directional pipe, but is backed by a DGRAM socket so each
519 # message can be easily separated.
520 o = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
521 self._mainipc, self._workeripc = o
515 self._servicehandler.bindsocket(self._sock, self.address)
522 self._servicehandler.bindsocket(self._sock, self.address)
516 if util.safehasattr(procutil, 'unblocksignal'):
523 if util.safehasattr(procutil, 'unblocksignal'):
517 procutil.unblocksignal(signal.SIGCHLD)
524 procutil.unblocksignal(signal.SIGCHLD)
518 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
525 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
519 self._oldsigchldhandler = o
526 self._oldsigchldhandler = o
520 self._socketunlinked = False
527 self._socketunlinked = False
521
528
522 def _unlinksocket(self):
529 def _unlinksocket(self):
523 if not self._socketunlinked:
530 if not self._socketunlinked:
524 self._servicehandler.unlinksocket(self.address)
531 self._servicehandler.unlinksocket(self.address)
525 self._socketunlinked = True
532 self._socketunlinked = True
526
533
527 def _cleanup(self):
534 def _cleanup(self):
528 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
535 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
529 self._sock.close()
536 self._sock.close()
537 self._mainipc.close()
538 self._workeripc.close()
530 self._unlinksocket()
539 self._unlinksocket()
531 # don't kill child processes as they have active clients, just wait
540 # don't kill child processes as they have active clients, just wait
532 self._reapworkers(0)
541 self._reapworkers(0)
533
542
534 def run(self):
543 def run(self):
535 try:
544 try:
536 self._mainloop()
545 self._mainloop()
537 finally:
546 finally:
538 self._cleanup()
547 self._cleanup()
539
548
540 def _mainloop(self):
549 def _mainloop(self):
541 exiting = False
550 exiting = False
542 h = self._servicehandler
551 h = self._servicehandler
543 selector = selectors.DefaultSelector()
552 selector = selectors.DefaultSelector()
544 selector.register(self._sock, selectors.EVENT_READ,
553 selector.register(self._sock, selectors.EVENT_READ,
545 self._acceptnewconnection)
554 self._acceptnewconnection)
555 selector.register(self._mainipc, selectors.EVENT_READ,
556 self._handlemainipc)
546 while True:
557 while True:
547 if not exiting and h.shouldexit():
558 if not exiting and h.shouldexit():
548 # clients can no longer connect() to the domain socket, so
559 # clients can no longer connect() to the domain socket, so
549 # we stop queuing new requests.
560 # we stop queuing new requests.
550 # for requests that are queued (connect()-ed, but haven't been
561 # for requests that are queued (connect()-ed, but haven't been
551 # accept()-ed), handle them before exit. otherwise, clients
562 # accept()-ed), handle them before exit. otherwise, clients
552 # waiting for recv() will receive ECONNRESET.
563 # waiting for recv() will receive ECONNRESET.
553 self._unlinksocket()
564 self._unlinksocket()
554 exiting = True
565 exiting = True
555 try:
566 try:
556 events = selector.select(timeout=h.pollinterval)
567 events = selector.select(timeout=h.pollinterval)
557 except OSError as inst:
568 except OSError as inst:
558 # selectors2 raises ETIMEDOUT if timeout exceeded while
569 # selectors2 raises ETIMEDOUT if timeout exceeded while
559 # handling signal interrupt. That's probably wrong, but
570 # handling signal interrupt. That's probably wrong, but
560 # we can easily get around it.
571 # we can easily get around it.
561 if inst.errno != errno.ETIMEDOUT:
572 if inst.errno != errno.ETIMEDOUT:
562 raise
573 raise
563 events = []
574 events = []
564 if not events:
575 if not events:
565 # only exit if we completed all queued requests
576 # only exit if we completed all queued requests
566 if exiting:
577 if exiting:
567 break
578 break
568 continue
579 continue
569 for key, _mask in events:
580 for key, _mask in events:
570 key.data(key.fileobj, selector)
581 key.data(key.fileobj, selector)
571 selector.close()
582 selector.close()
572
583
573 def _acceptnewconnection(self, sock, selector):
584 def _acceptnewconnection(self, sock, selector):
574 h = self._servicehandler
585 h = self._servicehandler
575 try:
586 try:
576 conn, _addr = sock.accept()
587 conn, _addr = sock.accept()
577 except socket.error as inst:
588 except socket.error as inst:
578 if inst.args[0] == errno.EINTR:
589 if inst.args[0] == errno.EINTR:
579 return
590 return
580 raise
591 raise
581
592
582 pid = os.fork()
593 pid = os.fork()
583 if pid:
594 if pid:
584 try:
595 try:
585 self.ui.log(b'cmdserver', b'forked worker process (pid=%d)\n',
596 self.ui.log(b'cmdserver', b'forked worker process (pid=%d)\n',
586 pid)
597 pid)
587 self._workerpids.add(pid)
598 self._workerpids.add(pid)
588 h.newconnection()
599 h.newconnection()
589 finally:
600 finally:
590 conn.close() # release handle in parent process
601 conn.close() # release handle in parent process
591 else:
602 else:
592 try:
603 try:
593 selector.close()
604 selector.close()
594 sock.close()
605 sock.close()
606 self._mainipc.close()
595 self._runworker(conn)
607 self._runworker(conn)
596 conn.close()
608 conn.close()
609 self._workeripc.close()
597 os._exit(0)
610 os._exit(0)
598 except: # never return, hence no re-raises
611 except: # never return, hence no re-raises
599 try:
612 try:
600 self.ui.traceback(force=True)
613 self.ui.traceback(force=True)
601 finally:
614 finally:
602 os._exit(255)
615 os._exit(255)
603
616
617 def _handlemainipc(self, sock, selector):
618 """Process messages sent from a worker"""
619 try:
620 path = sock.recv(32768) # large enough to receive path
621 except socket.error as inst:
622 if inst.args[0] == errno.EINTR:
623 return
624 raise
625
626 self.ui.log(b'cmdserver', b'repository: %s\n', path)
627
604 def _sigchldhandler(self, signal, frame):
628 def _sigchldhandler(self, signal, frame):
605 self._reapworkers(os.WNOHANG)
629 self._reapworkers(os.WNOHANG)
606
630
607 def _reapworkers(self, options):
631 def _reapworkers(self, options):
608 while self._workerpids:
632 while self._workerpids:
609 try:
633 try:
610 pid, _status = os.waitpid(-1, options)
634 pid, _status = os.waitpid(-1, options)
611 except OSError as inst:
635 except OSError as inst:
612 if inst.errno == errno.EINTR:
636 if inst.errno == errno.EINTR:
613 continue
637 continue
614 if inst.errno != errno.ECHILD:
638 if inst.errno != errno.ECHILD:
615 raise
639 raise
616 # no child processes at all (reaped by other waitpid()?)
640 # no child processes at all (reaped by other waitpid()?)
617 self._workerpids.clear()
641 self._workerpids.clear()
618 return
642 return
619 if pid == 0:
643 if pid == 0:
620 # no waitable child processes
644 # no waitable child processes
621 return
645 return
622 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
646 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
623 self._workerpids.discard(pid)
647 self._workerpids.discard(pid)
624
648
625 def _runworker(self, conn):
649 def _runworker(self, conn):
626 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
650 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
627 _initworkerprocess()
651 _initworkerprocess()
628 h = self._servicehandler
652 h = self._servicehandler
629 try:
653 try:
630 _serverequest(self.ui, self.repo, conn, h.createcmdserver,
654 _serverequest(self.ui, self.repo, conn, h.createcmdserver,
631 prereposetups=None) # TODO: pass in hook functions
655 prereposetups=[self._reposetup])
632 finally:
656 finally:
633 gc.collect() # trigger __del__ since worker process uses os._exit
657 gc.collect() # trigger __del__ since worker process uses os._exit
658
659 def _reposetup(self, ui, repo):
660 if not repo.local():
661 return
662
663 class unixcmdserverrepo(repo.__class__):
664 def close(self):
665 super(unixcmdserverrepo, self).close()
666 try:
667 self._cmdserveripc.send(self.root)
668 except socket.error:
669 self.ui.log(b'cmdserver',
670 b'failed to send repo root to master\n')
671
672 repo.__class__ = unixcmdserverrepo
673 repo._cmdserveripc = self._workeripc
@@ -1,242 +1,242
1 #require chg
1 #require chg
2
2
3 $ mkdir log
3 $ mkdir log
4 $ cat <<'EOF' >> $HGRCPATH
4 $ cat <<'EOF' >> $HGRCPATH
5 > [cmdserver]
5 > [cmdserver]
6 > log = $TESTTMP/log/server.log
6 > log = $TESTTMP/log/server.log
7 > max-log-files = 1
7 > max-log-files = 1
8 > max-log-size = 10 kB
8 > max-log-size = 10 kB
9 > EOF
9 > EOF
10 $ cp $HGRCPATH $HGRCPATH.orig
10 $ cp $HGRCPATH $HGRCPATH.orig
11
11
12 $ filterlog () {
12 $ filterlog () {
13 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
13 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
14 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
14 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
15 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
15 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
16 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
16 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
17 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
17 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
18 > }
18 > }
19
19
20 init repo
20 init repo
21
21
22 $ chg init foo
22 $ chg init foo
23 $ cd foo
23 $ cd foo
24
24
25 ill-formed config
25 ill-formed config
26
26
27 $ chg status
27 $ chg status
28 $ echo '=brokenconfig' >> $HGRCPATH
28 $ echo '=brokenconfig' >> $HGRCPATH
29 $ chg status
29 $ chg status
30 hg: parse error at * (glob)
30 hg: parse error at * (glob)
31 [255]
31 [255]
32
32
33 $ cp $HGRCPATH.orig $HGRCPATH
33 $ cp $HGRCPATH.orig $HGRCPATH
34
34
35 long socket path
35 long socket path
36
36
37 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
37 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
38 $ mkdir -p $sockpath
38 $ mkdir -p $sockpath
39 $ bakchgsockname=$CHGSOCKNAME
39 $ bakchgsockname=$CHGSOCKNAME
40 $ CHGSOCKNAME=$sockpath/server
40 $ CHGSOCKNAME=$sockpath/server
41 $ export CHGSOCKNAME
41 $ export CHGSOCKNAME
42 $ chg root
42 $ chg root
43 $TESTTMP/foo
43 $TESTTMP/foo
44 $ rm -rf $sockpath
44 $ rm -rf $sockpath
45 $ CHGSOCKNAME=$bakchgsockname
45 $ CHGSOCKNAME=$bakchgsockname
46 $ export CHGSOCKNAME
46 $ export CHGSOCKNAME
47
47
48 $ cd ..
48 $ cd ..
49
49
50 editor
50 editor
51 ------
51 ------
52
52
53 $ cat >> pushbuffer.py <<EOF
53 $ cat >> pushbuffer.py <<EOF
54 > def reposetup(ui, repo):
54 > def reposetup(ui, repo):
55 > repo.ui.pushbuffer(subproc=True)
55 > repo.ui.pushbuffer(subproc=True)
56 > EOF
56 > EOF
57
57
58 $ chg init editor
58 $ chg init editor
59 $ cd editor
59 $ cd editor
60
60
61 by default, system() should be redirected to the client:
61 by default, system() should be redirected to the client:
62
62
63 $ touch foo
63 $ touch foo
64 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
64 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
65 > | egrep "HG:|run 'cat"
65 > | egrep "HG:|run 'cat"
66 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
66 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
68 HG: Leave message empty to abort commit.
68 HG: Leave message empty to abort commit.
69 HG: --
69 HG: --
70 HG: user: test
70 HG: user: test
71 HG: branch 'default'
71 HG: branch 'default'
72 HG: added foo
72 HG: added foo
73
73
74 but no redirection should be made if output is captured:
74 but no redirection should be made if output is captured:
75
75
76 $ touch bar
76 $ touch bar
77 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
77 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
78 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
78 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
79 > | egrep "HG:|run 'cat"
79 > | egrep "HG:|run 'cat"
80 [1]
80 [1]
81
81
82 check that commit commands succeeded:
82 check that commit commands succeeded:
83
83
84 $ hg log -T '{rev}:{desc}\n'
84 $ hg log -T '{rev}:{desc}\n'
85 1:bufferred
85 1:bufferred
86 0:channeled
86 0:channeled
87
87
88 $ cd ..
88 $ cd ..
89
89
90 pager
90 pager
91 -----
91 -----
92
92
93 $ cat >> fakepager.py <<EOF
93 $ cat >> fakepager.py <<EOF
94 > import sys
94 > import sys
95 > for line in sys.stdin:
95 > for line in sys.stdin:
96 > sys.stdout.write('paged! %r\n' % line)
96 > sys.stdout.write('paged! %r\n' % line)
97 > EOF
97 > EOF
98
98
99 enable pager extension globally, but spawns the master server with no tty:
99 enable pager extension globally, but spawns the master server with no tty:
100
100
101 $ chg init pager
101 $ chg init pager
102 $ cd pager
102 $ cd pager
103 $ cat >> $HGRCPATH <<EOF
103 $ cat >> $HGRCPATH <<EOF
104 > [extensions]
104 > [extensions]
105 > pager =
105 > pager =
106 > [pager]
106 > [pager]
107 > pager = "$PYTHON" $TESTTMP/fakepager.py
107 > pager = "$PYTHON" $TESTTMP/fakepager.py
108 > EOF
108 > EOF
109 $ chg version > /dev/null
109 $ chg version > /dev/null
110 $ touch foo
110 $ touch foo
111 $ chg ci -qAm foo
111 $ chg ci -qAm foo
112
112
113 pager should be enabled if the attached client has a tty:
113 pager should be enabled if the attached client has a tty:
114
114
115 $ chg log -l1 -q --config ui.formatted=True
115 $ chg log -l1 -q --config ui.formatted=True
116 paged! '0:1f7b0de80e11\n'
116 paged! '0:1f7b0de80e11\n'
117 $ chg log -l1 -q --config ui.formatted=False
117 $ chg log -l1 -q --config ui.formatted=False
118 0:1f7b0de80e11
118 0:1f7b0de80e11
119
119
120 chg waits for pager if runcommand raises
120 chg waits for pager if runcommand raises
121
121
122 $ cat > $TESTTMP/crash.py <<EOF
122 $ cat > $TESTTMP/crash.py <<EOF
123 > from mercurial import registrar
123 > from mercurial import registrar
124 > cmdtable = {}
124 > cmdtable = {}
125 > command = registrar.command(cmdtable)
125 > command = registrar.command(cmdtable)
126 > @command(b'crash')
126 > @command(b'crash')
127 > def pagercrash(ui, repo, *pats, **opts):
127 > def pagercrash(ui, repo, *pats, **opts):
128 > ui.write('going to crash\n')
128 > ui.write('going to crash\n')
129 > raise Exception('.')
129 > raise Exception('.')
130 > EOF
130 > EOF
131
131
132 $ cat > $TESTTMP/fakepager.py <<EOF
132 $ cat > $TESTTMP/fakepager.py <<EOF
133 > from __future__ import absolute_import
133 > from __future__ import absolute_import
134 > import sys
134 > import sys
135 > import time
135 > import time
136 > for line in iter(sys.stdin.readline, ''):
136 > for line in iter(sys.stdin.readline, ''):
137 > if 'crash' in line: # only interested in lines containing 'crash'
137 > if 'crash' in line: # only interested in lines containing 'crash'
138 > # if chg exits when pager is sleeping (incorrectly), the output
138 > # if chg exits when pager is sleeping (incorrectly), the output
139 > # will be captured by the next test case
139 > # will be captured by the next test case
140 > time.sleep(1)
140 > time.sleep(1)
141 > sys.stdout.write('crash-pager: %s' % line)
141 > sys.stdout.write('crash-pager: %s' % line)
142 > EOF
142 > EOF
143
143
144 $ cat >> .hg/hgrc <<EOF
144 $ cat >> .hg/hgrc <<EOF
145 > [extensions]
145 > [extensions]
146 > crash = $TESTTMP/crash.py
146 > crash = $TESTTMP/crash.py
147 > EOF
147 > EOF
148
148
149 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
149 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
150 crash-pager: going to crash
150 crash-pager: going to crash
151 [255]
151 [255]
152
152
153 $ cd ..
153 $ cd ..
154
154
155 server lifecycle
155 server lifecycle
156 ----------------
156 ----------------
157
157
158 chg server should be restarted on code change, and old server will shut down
158 chg server should be restarted on code change, and old server will shut down
159 automatically. In this test, we use the following time parameters:
159 automatically. In this test, we use the following time parameters:
160
160
161 - "sleep 1" to make mtime different
161 - "sleep 1" to make mtime different
162 - "sleep 2" to notice mtime change (polling interval is 1 sec)
162 - "sleep 2" to notice mtime change (polling interval is 1 sec)
163
163
164 set up repository with an extension:
164 set up repository with an extension:
165
165
166 $ chg init extreload
166 $ chg init extreload
167 $ cd extreload
167 $ cd extreload
168 $ touch dummyext.py
168 $ touch dummyext.py
169 $ cat <<EOF >> .hg/hgrc
169 $ cat <<EOF >> .hg/hgrc
170 > [extensions]
170 > [extensions]
171 > dummyext = dummyext.py
171 > dummyext = dummyext.py
172 > EOF
172 > EOF
173
173
174 isolate socket directory for stable result:
174 isolate socket directory for stable result:
175
175
176 $ OLDCHGSOCKNAME=$CHGSOCKNAME
176 $ OLDCHGSOCKNAME=$CHGSOCKNAME
177 $ mkdir chgsock
177 $ mkdir chgsock
178 $ CHGSOCKNAME=`pwd`/chgsock/server
178 $ CHGSOCKNAME=`pwd`/chgsock/server
179
179
180 warm up server:
180 warm up server:
181
181
182 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
182 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
183 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
183 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
184
184
185 new server should be started if extension modified:
185 new server should be started if extension modified:
186
186
187 $ sleep 1
187 $ sleep 1
188 $ touch dummyext.py
188 $ touch dummyext.py
189 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
189 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
190 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
190 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
191 chg: debug: * instruction: reconnect (glob)
191 chg: debug: * instruction: reconnect (glob)
192 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
192 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
193
193
194 old server will shut down, while new server should still be reachable:
194 old server will shut down, while new server should still be reachable:
195
195
196 $ sleep 2
196 $ sleep 2
197 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
197 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
198
198
199 socket file should never be unlinked by old server:
199 socket file should never be unlinked by old server:
200 (simulates unowned socket by updating mtime, which makes sure server exits
200 (simulates unowned socket by updating mtime, which makes sure server exits
201 at polling cycle)
201 at polling cycle)
202
202
203 $ ls chgsock/server-*
203 $ ls chgsock/server-*
204 chgsock/server-* (glob)
204 chgsock/server-* (glob)
205 $ touch chgsock/server-*
205 $ touch chgsock/server-*
206 $ sleep 2
206 $ sleep 2
207 $ ls chgsock/server-*
207 $ ls chgsock/server-*
208 chgsock/server-* (glob)
208 chgsock/server-* (glob)
209
209
210 since no server is reachable from socket file, new server should be started:
210 since no server is reachable from socket file, new server should be started:
211 (this test makes sure that old server shut down automatically)
211 (this test makes sure that old server shut down automatically)
212
212
213 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
213 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
214 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
214 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
215
215
216 shut down servers and restore environment:
216 shut down servers and restore environment:
217
217
218 $ rm -R chgsock
218 $ rm -R chgsock
219 $ sleep 2
219 $ sleep 2
220 $ CHGSOCKNAME=$OLDCHGSOCKNAME
220 $ CHGSOCKNAME=$OLDCHGSOCKNAME
221 $ cd ..
221 $ cd ..
222
222
223 check that server events are recorded:
223 check that server events are recorded:
224
224
225 $ ls log
225 $ ls log
226 server.log
226 server.log
227 server.log.1
227 server.log.1
228
228
229 print only the last 10 lines, since we aren't sure how many records are
229 print only the last 10 lines, since we aren't sure how many records are
230 preserved:
230 preserved:
231
231
232 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
232 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
233 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
234 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ...
233 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ...
235 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
234 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
236 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
235 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
237 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
236 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
238 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
237 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
239 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
238 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
240 YYYY/MM/DD HH:MM:SS (PID)> validate: []
239 YYYY/MM/DD HH:MM:SS (PID)> validate: []
240 YYYY/MM/DD HH:MM:SS (PID)> repository: $TESTTMP/extreload
241 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
241 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
242 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
242 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
General Comments 0
You need to be logged in to leave comments. Login now