comparison mercurial/commandserver.py @ 45058:d6e99a446eea

cmdserver: add option to not exit from message loop on SIGINT Sending SIGINT to server is the only way to interrupt a command running in command-server process. SIGINT will be caught at dispatch.dispatch() if we're lucky. Otherwise it will terminate the serer process. This is fundamentally unreliable as signals are delivered asynchronously. "cmdserver.shutdown-on-interrupt=False" mitigate the issue by making the server basically block SIGINT.
author Yuya Nishihara <yuya@tcha.org>
date Sat, 27 Jun 2020 21:46:23 +0900
parents f5c006621f07
children f43bc4ce0d69
comparison
equal deleted inserted replaced
45057:9694895749ad 45058:d6e99a446eea
242 encname, encfn = _selectmessageencoder(ui) 242 encname, encfn = _selectmessageencoder(ui)
243 self.cmsg = channeledmessage(fout, b'm', encname, encfn) 243 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
244 244
245 self.client = fin 245 self.client = fin
246 246
247 # If shutdown-on-interrupt is off, the default SIGINT handler is
248 # removed so that client-server communication wouldn't be interrupted.
249 # For example, 'runcommand' handler will issue three short read()s.
250 # If one of the first two read()s were interrupted, the communication
251 # channel would be left at dirty state and the subsequent request
252 # wouldn't be parsed. So catching KeyboardInterrupt isn't enough.
253 self._shutdown_on_interrupt = ui.configbool(
254 b'cmdserver', b'shutdown-on-interrupt'
255 )
256 self._old_inthandler = None
257 if not self._shutdown_on_interrupt:
258 self._old_inthandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
259
247 def cleanup(self): 260 def cleanup(self):
248 """release and restore resources taken during server session""" 261 """release and restore resources taken during server session"""
262 if not self._shutdown_on_interrupt:
263 signal.signal(signal.SIGINT, self._old_inthandler)
249 264
250 def _read(self, size): 265 def _read(self, size):
251 if not size: 266 if not size:
252 return b'' 267 return b''
253 268
275 s = self._readstr() 290 s = self._readstr()
276 if s: 291 if s:
277 return s.split(b'\0') 292 return s.split(b'\0')
278 else: 293 else:
279 return [] 294 return []
295
296 def _dispatchcommand(self, req):
297 from . import dispatch # avoid cycle
298
299 if self._shutdown_on_interrupt:
300 # no need to restore SIGINT handler as it is unmodified.
301 return dispatch.dispatch(req)
302
303 try:
304 signal.signal(signal.SIGINT, self._old_inthandler)
305 return dispatch.dispatch(req)
306 except error.SignalInterrupt:
307 # propagate SIGBREAK, SIGHUP, or SIGTERM.
308 raise
309 except KeyboardInterrupt:
310 # SIGINT may be received out of the try-except block of dispatch(),
311 # so catch it as last ditch. Another KeyboardInterrupt may be
312 # raised while handling exceptions here, but there's no way to
313 # avoid that except for doing everything in C.
314 pass
315 finally:
316 signal.signal(signal.SIGINT, signal.SIG_IGN)
317 # On KeyboardInterrupt, print error message and exit *after* SIGINT
318 # handler removed.
319 req.ui.error(_(b'interrupted!\n'))
320 return -1
280 321
281 def runcommand(self): 322 def runcommand(self):
282 """ reads a list of \0 terminated arguments, executes 323 """ reads a list of \0 terminated arguments, executes
283 and writes the return code to the result channel """ 324 and writes the return code to the result channel """
284 from . import dispatch # avoid cycle 325 from . import dispatch # avoid cycle
316 self.cmsg, 357 self.cmsg,
317 prereposetups=self._prereposetups, 358 prereposetups=self._prereposetups,
318 ) 359 )
319 360
320 try: 361 try:
321 ret = dispatch.dispatch(req) & 255 362 ret = self._dispatchcommand(req) & 255
363 # If shutdown-on-interrupt is off, it's important to write the
364 # result code *after* SIGINT handler removed. If the result code
365 # were lost, the client wouldn't be able to continue processing.
322 self.cresult.write(struct.pack(b'>i', int(ret))) 366 self.cresult.write(struct.pack(b'>i', int(ret)))
323 finally: 367 finally:
324 # restore old cwd 368 # restore old cwd
325 if b'--cwd' in args: 369 if b'--cwd' in args:
326 os.chdir(self.cwd) 370 os.chdir(self.cwd)