##// END OF EJS Templates
procutil: move protectio/restoreio from commandserver...
Yuya Nishihara -
r37141:0216232f default
parent child Browse files
Show More
@@ -1,565 +1,536 b''
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 pycompat,
29 pycompat,
30 util,
30 util,
31 )
31 )
32 from .utils import (
32 from .utils import (
33 procutil,
33 procutil,
34 )
34 )
35
35
36 logfile = None
36 logfile = None
37
37
38 def log(*args):
38 def log(*args):
39 if not logfile:
39 if not logfile:
40 return
40 return
41
41
42 for a in args:
42 for a in args:
43 logfile.write(str(a))
43 logfile.write(str(a))
44
44
45 logfile.flush()
45 logfile.flush()
46
46
47 class channeledoutput(object):
47 class channeledoutput(object):
48 """
48 """
49 Write data to out in the following format:
49 Write data to out in the following format:
50
50
51 data length (unsigned int),
51 data length (unsigned int),
52 data
52 data
53 """
53 """
54 def __init__(self, out, channel):
54 def __init__(self, out, channel):
55 self.out = out
55 self.out = out
56 self.channel = channel
56 self.channel = channel
57
57
58 @property
58 @property
59 def name(self):
59 def name(self):
60 return '<%c-channel>' % self.channel
60 return '<%c-channel>' % self.channel
61
61
62 def write(self, data):
62 def write(self, data):
63 if not data:
63 if not data:
64 return
64 return
65 # single write() to guarantee the same atomicity as the underlying file
65 # single write() to guarantee the same atomicity as the underlying file
66 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
66 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
67 self.out.flush()
67 self.out.flush()
68
68
69 def __getattr__(self, attr):
69 def __getattr__(self, attr):
70 if attr in ('isatty', 'fileno', 'tell', 'seek'):
70 if attr in ('isatty', 'fileno', 'tell', 'seek'):
71 raise AttributeError(attr)
71 raise AttributeError(attr)
72 return getattr(self.out, attr)
72 return getattr(self.out, attr)
73
73
74 class channeledinput(object):
74 class channeledinput(object):
75 """
75 """
76 Read data from in_.
76 Read data from in_.
77
77
78 Requests for input are written to out in the following format:
78 Requests for input are written to out in the following format:
79 channel identifier - 'I' for plain input, 'L' line based (1 byte)
79 channel identifier - 'I' for plain input, 'L' line based (1 byte)
80 how many bytes to send at most (unsigned int),
80 how many bytes to send at most (unsigned int),
81
81
82 The client replies with:
82 The client replies with:
83 data length (unsigned int), 0 meaning EOF
83 data length (unsigned int), 0 meaning EOF
84 data
84 data
85 """
85 """
86
86
87 maxchunksize = 4 * 1024
87 maxchunksize = 4 * 1024
88
88
89 def __init__(self, in_, out, channel):
89 def __init__(self, in_, out, channel):
90 self.in_ = in_
90 self.in_ = in_
91 self.out = out
91 self.out = out
92 self.channel = channel
92 self.channel = channel
93
93
94 @property
94 @property
95 def name(self):
95 def name(self):
96 return '<%c-channel>' % self.channel
96 return '<%c-channel>' % self.channel
97
97
98 def read(self, size=-1):
98 def read(self, size=-1):
99 if size < 0:
99 if size < 0:
100 # if we need to consume all the clients input, ask for 4k chunks
100 # if we need to consume all the clients input, ask for 4k chunks
101 # so the pipe doesn't fill up risking a deadlock
101 # so the pipe doesn't fill up risking a deadlock
102 size = self.maxchunksize
102 size = self.maxchunksize
103 s = self._read(size, self.channel)
103 s = self._read(size, self.channel)
104 buf = s
104 buf = s
105 while s:
105 while s:
106 s = self._read(size, self.channel)
106 s = self._read(size, self.channel)
107 buf += s
107 buf += s
108
108
109 return buf
109 return buf
110 else:
110 else:
111 return self._read(size, self.channel)
111 return self._read(size, self.channel)
112
112
113 def _read(self, size, channel):
113 def _read(self, size, channel):
114 if not size:
114 if not size:
115 return ''
115 return ''
116 assert size > 0
116 assert size > 0
117
117
118 # tell the client we need at most size bytes
118 # tell the client we need at most size bytes
119 self.out.write(struct.pack('>cI', channel, size))
119 self.out.write(struct.pack('>cI', channel, size))
120 self.out.flush()
120 self.out.flush()
121
121
122 length = self.in_.read(4)
122 length = self.in_.read(4)
123 length = struct.unpack('>I', length)[0]
123 length = struct.unpack('>I', length)[0]
124 if not length:
124 if not length:
125 return ''
125 return ''
126 else:
126 else:
127 return self.in_.read(length)
127 return self.in_.read(length)
128
128
129 def readline(self, size=-1):
129 def readline(self, size=-1):
130 if size < 0:
130 if size < 0:
131 size = self.maxchunksize
131 size = self.maxchunksize
132 s = self._read(size, 'L')
132 s = self._read(size, 'L')
133 buf = s
133 buf = s
134 # keep asking for more until there's either no more or
134 # keep asking for more until there's either no more or
135 # we got a full line
135 # we got a full line
136 while s and s[-1] != '\n':
136 while s and s[-1] != '\n':
137 s = self._read(size, 'L')
137 s = self._read(size, 'L')
138 buf += s
138 buf += s
139
139
140 return buf
140 return buf
141 else:
141 else:
142 return self._read(size, 'L')
142 return self._read(size, 'L')
143
143
144 def __iter__(self):
144 def __iter__(self):
145 return self
145 return self
146
146
147 def next(self):
147 def next(self):
148 l = self.readline()
148 l = self.readline()
149 if not l:
149 if not l:
150 raise StopIteration
150 raise StopIteration
151 return l
151 return l
152
152
153 def __getattr__(self, attr):
153 def __getattr__(self, attr):
154 if attr in ('isatty', 'fileno', 'tell', 'seek'):
154 if attr in ('isatty', 'fileno', 'tell', 'seek'):
155 raise AttributeError(attr)
155 raise AttributeError(attr)
156 return getattr(self.in_, attr)
156 return getattr(self.in_, attr)
157
157
158 class server(object):
158 class server(object):
159 """
159 """
160 Listens for commands on fin, runs them and writes the output on a channel
160 Listens for commands on fin, runs them and writes the output on a channel
161 based stream to fout.
161 based stream to fout.
162 """
162 """
163 def __init__(self, ui, repo, fin, fout):
163 def __init__(self, ui, repo, fin, fout):
164 self.cwd = pycompat.getcwd()
164 self.cwd = pycompat.getcwd()
165
165
166 # developer config: cmdserver.log
166 # developer config: cmdserver.log
167 logpath = ui.config("cmdserver", "log")
167 logpath = ui.config("cmdserver", "log")
168 if logpath:
168 if logpath:
169 global logfile
169 global logfile
170 if logpath == '-':
170 if logpath == '-':
171 # write log on a special 'd' (debug) channel
171 # write log on a special 'd' (debug) channel
172 logfile = channeledoutput(fout, 'd')
172 logfile = channeledoutput(fout, 'd')
173 else:
173 else:
174 logfile = open(logpath, 'a')
174 logfile = open(logpath, 'a')
175
175
176 if repo:
176 if repo:
177 # the ui here is really the repo ui so take its baseui so we don't
177 # the ui here is really the repo ui so take its baseui so we don't
178 # end up with its local configuration
178 # end up with its local configuration
179 self.ui = repo.baseui
179 self.ui = repo.baseui
180 self.repo = repo
180 self.repo = repo
181 self.repoui = repo.ui
181 self.repoui = repo.ui
182 else:
182 else:
183 self.ui = ui
183 self.ui = ui
184 self.repo = self.repoui = None
184 self.repo = self.repoui = None
185
185
186 self.cerr = channeledoutput(fout, 'e')
186 self.cerr = channeledoutput(fout, 'e')
187 self.cout = channeledoutput(fout, 'o')
187 self.cout = channeledoutput(fout, 'o')
188 self.cin = channeledinput(fin, fout, 'I')
188 self.cin = channeledinput(fin, fout, 'I')
189 self.cresult = channeledoutput(fout, 'r')
189 self.cresult = channeledoutput(fout, 'r')
190
190
191 self.client = fin
191 self.client = fin
192
192
193 def cleanup(self):
193 def cleanup(self):
194 """release and restore resources taken during server session"""
194 """release and restore resources taken during server session"""
195
195
196 def _read(self, size):
196 def _read(self, size):
197 if not size:
197 if not size:
198 return ''
198 return ''
199
199
200 data = self.client.read(size)
200 data = self.client.read(size)
201
201
202 # is the other end closed?
202 # is the other end closed?
203 if not data:
203 if not data:
204 raise EOFError
204 raise EOFError
205
205
206 return data
206 return data
207
207
208 def _readstr(self):
208 def _readstr(self):
209 """read a string from the channel
209 """read a string from the channel
210
210
211 format:
211 format:
212 data length (uint32), data
212 data length (uint32), data
213 """
213 """
214 length = struct.unpack('>I', self._read(4))[0]
214 length = struct.unpack('>I', self._read(4))[0]
215 if not length:
215 if not length:
216 return ''
216 return ''
217 return self._read(length)
217 return self._read(length)
218
218
219 def _readlist(self):
219 def _readlist(self):
220 """read a list of NULL separated strings from the channel"""
220 """read a list of NULL separated strings from the channel"""
221 s = self._readstr()
221 s = self._readstr()
222 if s:
222 if s:
223 return s.split('\0')
223 return s.split('\0')
224 else:
224 else:
225 return []
225 return []
226
226
227 def runcommand(self):
227 def runcommand(self):
228 """ reads a list of \0 terminated arguments, executes
228 """ reads a list of \0 terminated arguments, executes
229 and writes the return code to the result channel """
229 and writes the return code to the result channel """
230 from . import dispatch # avoid cycle
230 from . import dispatch # avoid cycle
231
231
232 args = self._readlist()
232 args = self._readlist()
233
233
234 # copy the uis so changes (e.g. --config or --verbose) don't
234 # copy the uis so changes (e.g. --config or --verbose) don't
235 # persist between requests
235 # persist between requests
236 copiedui = self.ui.copy()
236 copiedui = self.ui.copy()
237 uis = [copiedui]
237 uis = [copiedui]
238 if self.repo:
238 if self.repo:
239 self.repo.baseui = copiedui
239 self.repo.baseui = copiedui
240 # clone ui without using ui.copy because this is protected
240 # clone ui without using ui.copy because this is protected
241 repoui = self.repoui.__class__(self.repoui)
241 repoui = self.repoui.__class__(self.repoui)
242 repoui.copy = copiedui.copy # redo copy protection
242 repoui.copy = copiedui.copy # redo copy protection
243 uis.append(repoui)
243 uis.append(repoui)
244 self.repo.ui = self.repo.dirstate._ui = repoui
244 self.repo.ui = self.repo.dirstate._ui = repoui
245 self.repo.invalidateall()
245 self.repo.invalidateall()
246
246
247 for ui in uis:
247 for ui in uis:
248 ui.resetstate()
248 ui.resetstate()
249 # any kind of interaction must use server channels, but chg may
249 # any kind of interaction must use server channels, but chg may
250 # replace channels by fully functional tty files. so nontty is
250 # replace channels by fully functional tty files. so nontty is
251 # enforced only if cin is a channel.
251 # enforced only if cin is a channel.
252 if not util.safehasattr(self.cin, 'fileno'):
252 if not util.safehasattr(self.cin, 'fileno'):
253 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
253 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
254
254
255 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
255 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
256 self.cout, self.cerr)
256 self.cout, self.cerr)
257
257
258 try:
258 try:
259 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
259 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
260 self.cresult.write(struct.pack('>i', int(ret)))
260 self.cresult.write(struct.pack('>i', int(ret)))
261 finally:
261 finally:
262 # restore old cwd
262 # restore old cwd
263 if '--cwd' in args:
263 if '--cwd' in args:
264 os.chdir(self.cwd)
264 os.chdir(self.cwd)
265
265
266 def getencoding(self):
266 def getencoding(self):
267 """ writes the current encoding to the result channel """
267 """ writes the current encoding to the result channel """
268 self.cresult.write(encoding.encoding)
268 self.cresult.write(encoding.encoding)
269
269
270 def serveone(self):
270 def serveone(self):
271 cmd = self.client.readline()[:-1]
271 cmd = self.client.readline()[:-1]
272 if cmd:
272 if cmd:
273 handler = self.capabilities.get(cmd)
273 handler = self.capabilities.get(cmd)
274 if handler:
274 if handler:
275 handler(self)
275 handler(self)
276 else:
276 else:
277 # clients are expected to check what commands are supported by
277 # clients are expected to check what commands are supported by
278 # looking at the servers capabilities
278 # looking at the servers capabilities
279 raise error.Abort(_('unknown command %s') % cmd)
279 raise error.Abort(_('unknown command %s') % cmd)
280
280
281 return cmd != ''
281 return cmd != ''
282
282
283 capabilities = {'runcommand': runcommand,
283 capabilities = {'runcommand': runcommand,
284 'getencoding': getencoding}
284 'getencoding': getencoding}
285
285
286 def serve(self):
286 def serve(self):
287 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
287 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
288 hellomsg += '\n'
288 hellomsg += '\n'
289 hellomsg += 'encoding: ' + encoding.encoding
289 hellomsg += 'encoding: ' + encoding.encoding
290 hellomsg += '\n'
290 hellomsg += '\n'
291 hellomsg += 'pid: %d' % procutil.getpid()
291 hellomsg += 'pid: %d' % procutil.getpid()
292 if util.safehasattr(os, 'getpgid'):
292 if util.safehasattr(os, 'getpgid'):
293 hellomsg += '\n'
293 hellomsg += '\n'
294 hellomsg += 'pgid: %d' % os.getpgid(0)
294 hellomsg += 'pgid: %d' % os.getpgid(0)
295
295
296 # write the hello msg in -one- chunk
296 # write the hello msg in -one- chunk
297 self.cout.write(hellomsg)
297 self.cout.write(hellomsg)
298
298
299 try:
299 try:
300 while self.serveone():
300 while self.serveone():
301 pass
301 pass
302 except EOFError:
302 except EOFError:
303 # we'll get here if the client disconnected while we were reading
303 # we'll get here if the client disconnected while we were reading
304 # its request
304 # its request
305 return 1
305 return 1
306
306
307 return 0
307 return 0
308
308
309 def _protectio(uin, uout):
310 """Duplicate streams and redirect original to null if (uin, uout) are
311 stdio
312
313 Returns (fin, fout) which point to the original (uin, uout) fds, but
314 may be copy of (uin, uout). The returned streams can be considered
315 "owned" in that print(), exec(), etc. never reach to them.
316 """
317 uout.flush()
318 newfiles = []
319 nullfd = os.open(os.devnull, os.O_RDWR)
320 for f, sysf, mode in [(uin, procutil.stdin, r'rb'),
321 (uout, procutil.stdout, r'wb')]:
322 if f is sysf:
323 newfd = os.dup(f.fileno())
324 os.dup2(nullfd, f.fileno())
325 f = os.fdopen(newfd, mode)
326 newfiles.append(f)
327 os.close(nullfd)
328 return tuple(newfiles)
329
330 def _restoreio(uin, uout, fin, fout):
331 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
332 uout.flush()
333 for f, uif in [(fin, uin), (fout, uout)]:
334 if f is not uif:
335 os.dup2(f.fileno(), uif.fileno())
336 f.close()
337
338 class pipeservice(object):
309 class pipeservice(object):
339 def __init__(self, ui, repo, opts):
310 def __init__(self, ui, repo, opts):
340 self.ui = ui
311 self.ui = ui
341 self.repo = repo
312 self.repo = repo
342
313
343 def init(self):
314 def init(self):
344 pass
315 pass
345
316
346 def run(self):
317 def run(self):
347 ui = self.ui
318 ui = self.ui
348 # redirect stdio to null device so that broken extensions or in-process
319 # redirect stdio to null device so that broken extensions or in-process
349 # hooks will never cause corruption of channel protocol.
320 # hooks will never cause corruption of channel protocol.
350 fin, fout = _protectio(ui.fin, ui.fout)
321 fin, fout = procutil.protectstdio(ui.fin, ui.fout)
351 try:
322 try:
352 sv = server(ui, self.repo, fin, fout)
323 sv = server(ui, self.repo, fin, fout)
353 return sv.serve()
324 return sv.serve()
354 finally:
325 finally:
355 sv.cleanup()
326 sv.cleanup()
356 _restoreio(ui.fin, ui.fout, fin, fout)
327 procutil.restorestdio(ui.fin, ui.fout, fin, fout)
357
328
358 def _initworkerprocess():
329 def _initworkerprocess():
359 # use a different process group from the master process, in order to:
330 # use a different process group from the master process, in order to:
360 # 1. make the current process group no longer "orphaned" (because the
331 # 1. make the current process group no longer "orphaned" (because the
361 # parent of this process is in a different process group while
332 # parent of this process is in a different process group while
362 # remains in a same session)
333 # remains in a same session)
363 # according to POSIX 2.2.2.52, orphaned process group will ignore
334 # according to POSIX 2.2.2.52, orphaned process group will ignore
364 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
335 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
365 # cause trouble for things like ncurses.
336 # cause trouble for things like ncurses.
366 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
337 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
367 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
338 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
368 # processes like ssh will be killed properly, without affecting
339 # processes like ssh will be killed properly, without affecting
369 # unrelated processes.
340 # unrelated processes.
370 os.setpgid(0, 0)
341 os.setpgid(0, 0)
371 # change random state otherwise forked request handlers would have a
342 # change random state otherwise forked request handlers would have a
372 # same state inherited from parent.
343 # same state inherited from parent.
373 random.seed()
344 random.seed()
374
345
375 def _serverequest(ui, repo, conn, createcmdserver):
346 def _serverequest(ui, repo, conn, createcmdserver):
376 fin = conn.makefile('rb')
347 fin = conn.makefile('rb')
377 fout = conn.makefile('wb')
348 fout = conn.makefile('wb')
378 sv = None
349 sv = None
379 try:
350 try:
380 sv = createcmdserver(repo, conn, fin, fout)
351 sv = createcmdserver(repo, conn, fin, fout)
381 try:
352 try:
382 sv.serve()
353 sv.serve()
383 # handle exceptions that may be raised by command server. most of
354 # handle exceptions that may be raised by command server. most of
384 # known exceptions are caught by dispatch.
355 # known exceptions are caught by dispatch.
385 except error.Abort as inst:
356 except error.Abort as inst:
386 ui.warn(_('abort: %s\n') % inst)
357 ui.warn(_('abort: %s\n') % inst)
387 except IOError as inst:
358 except IOError as inst:
388 if inst.errno != errno.EPIPE:
359 if inst.errno != errno.EPIPE:
389 raise
360 raise
390 except KeyboardInterrupt:
361 except KeyboardInterrupt:
391 pass
362 pass
392 finally:
363 finally:
393 sv.cleanup()
364 sv.cleanup()
394 except: # re-raises
365 except: # re-raises
395 # also write traceback to error channel. otherwise client cannot
366 # also write traceback to error channel. otherwise client cannot
396 # see it because it is written to server's stderr by default.
367 # see it because it is written to server's stderr by default.
397 if sv:
368 if sv:
398 cerr = sv.cerr
369 cerr = sv.cerr
399 else:
370 else:
400 cerr = channeledoutput(fout, 'e')
371 cerr = channeledoutput(fout, 'e')
401 traceback.print_exc(file=cerr)
372 traceback.print_exc(file=cerr)
402 raise
373 raise
403 finally:
374 finally:
404 fin.close()
375 fin.close()
405 try:
376 try:
406 fout.close() # implicit flush() may cause another EPIPE
377 fout.close() # implicit flush() may cause another EPIPE
407 except IOError as inst:
378 except IOError as inst:
408 if inst.errno != errno.EPIPE:
379 if inst.errno != errno.EPIPE:
409 raise
380 raise
410
381
411 class unixservicehandler(object):
382 class unixservicehandler(object):
412 """Set of pluggable operations for unix-mode services
383 """Set of pluggable operations for unix-mode services
413
384
414 Almost all methods except for createcmdserver() are called in the main
385 Almost all methods except for createcmdserver() are called in the main
415 process. You can't pass mutable resource back from createcmdserver().
386 process. You can't pass mutable resource back from createcmdserver().
416 """
387 """
417
388
418 pollinterval = None
389 pollinterval = None
419
390
420 def __init__(self, ui):
391 def __init__(self, ui):
421 self.ui = ui
392 self.ui = ui
422
393
423 def bindsocket(self, sock, address):
394 def bindsocket(self, sock, address):
424 util.bindunixsocket(sock, address)
395 util.bindunixsocket(sock, address)
425 sock.listen(socket.SOMAXCONN)
396 sock.listen(socket.SOMAXCONN)
426 self.ui.status(_('listening at %s\n') % address)
397 self.ui.status(_('listening at %s\n') % address)
427 self.ui.flush() # avoid buffering of status message
398 self.ui.flush() # avoid buffering of status message
428
399
429 def unlinksocket(self, address):
400 def unlinksocket(self, address):
430 os.unlink(address)
401 os.unlink(address)
431
402
432 def shouldexit(self):
403 def shouldexit(self):
433 """True if server should shut down; checked per pollinterval"""
404 """True if server should shut down; checked per pollinterval"""
434 return False
405 return False
435
406
436 def newconnection(self):
407 def newconnection(self):
437 """Called when main process notices new connection"""
408 """Called when main process notices new connection"""
438
409
439 def createcmdserver(self, repo, conn, fin, fout):
410 def createcmdserver(self, repo, conn, fin, fout):
440 """Create new command server instance; called in the process that
411 """Create new command server instance; called in the process that
441 serves for the current connection"""
412 serves for the current connection"""
442 return server(self.ui, repo, fin, fout)
413 return server(self.ui, repo, fin, fout)
443
414
444 class unixforkingservice(object):
415 class unixforkingservice(object):
445 """
416 """
446 Listens on unix domain socket and forks server per connection
417 Listens on unix domain socket and forks server per connection
447 """
418 """
448
419
449 def __init__(self, ui, repo, opts, handler=None):
420 def __init__(self, ui, repo, opts, handler=None):
450 self.ui = ui
421 self.ui = ui
451 self.repo = repo
422 self.repo = repo
452 self.address = opts['address']
423 self.address = opts['address']
453 if not util.safehasattr(socket, 'AF_UNIX'):
424 if not util.safehasattr(socket, 'AF_UNIX'):
454 raise error.Abort(_('unsupported platform'))
425 raise error.Abort(_('unsupported platform'))
455 if not self.address:
426 if not self.address:
456 raise error.Abort(_('no socket path specified with --address'))
427 raise error.Abort(_('no socket path specified with --address'))
457 self._servicehandler = handler or unixservicehandler(ui)
428 self._servicehandler = handler or unixservicehandler(ui)
458 self._sock = None
429 self._sock = None
459 self._oldsigchldhandler = None
430 self._oldsigchldhandler = None
460 self._workerpids = set() # updated by signal handler; do not iterate
431 self._workerpids = set() # updated by signal handler; do not iterate
461 self._socketunlinked = None
432 self._socketunlinked = None
462
433
463 def init(self):
434 def init(self):
464 self._sock = socket.socket(socket.AF_UNIX)
435 self._sock = socket.socket(socket.AF_UNIX)
465 self._servicehandler.bindsocket(self._sock, self.address)
436 self._servicehandler.bindsocket(self._sock, self.address)
466 if util.safehasattr(procutil, 'unblocksignal'):
437 if util.safehasattr(procutil, 'unblocksignal'):
467 procutil.unblocksignal(signal.SIGCHLD)
438 procutil.unblocksignal(signal.SIGCHLD)
468 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
439 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
469 self._oldsigchldhandler = o
440 self._oldsigchldhandler = o
470 self._socketunlinked = False
441 self._socketunlinked = False
471
442
472 def _unlinksocket(self):
443 def _unlinksocket(self):
473 if not self._socketunlinked:
444 if not self._socketunlinked:
474 self._servicehandler.unlinksocket(self.address)
445 self._servicehandler.unlinksocket(self.address)
475 self._socketunlinked = True
446 self._socketunlinked = True
476
447
477 def _cleanup(self):
448 def _cleanup(self):
478 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
449 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
479 self._sock.close()
450 self._sock.close()
480 self._unlinksocket()
451 self._unlinksocket()
481 # don't kill child processes as they have active clients, just wait
452 # don't kill child processes as they have active clients, just wait
482 self._reapworkers(0)
453 self._reapworkers(0)
483
454
484 def run(self):
455 def run(self):
485 try:
456 try:
486 self._mainloop()
457 self._mainloop()
487 finally:
458 finally:
488 self._cleanup()
459 self._cleanup()
489
460
490 def _mainloop(self):
461 def _mainloop(self):
491 exiting = False
462 exiting = False
492 h = self._servicehandler
463 h = self._servicehandler
493 selector = selectors.DefaultSelector()
464 selector = selectors.DefaultSelector()
494 selector.register(self._sock, selectors.EVENT_READ)
465 selector.register(self._sock, selectors.EVENT_READ)
495 while True:
466 while True:
496 if not exiting and h.shouldexit():
467 if not exiting and h.shouldexit():
497 # clients can no longer connect() to the domain socket, so
468 # clients can no longer connect() to the domain socket, so
498 # we stop queuing new requests.
469 # we stop queuing new requests.
499 # for requests that are queued (connect()-ed, but haven't been
470 # for requests that are queued (connect()-ed, but haven't been
500 # accept()-ed), handle them before exit. otherwise, clients
471 # accept()-ed), handle them before exit. otherwise, clients
501 # waiting for recv() will receive ECONNRESET.
472 # waiting for recv() will receive ECONNRESET.
502 self._unlinksocket()
473 self._unlinksocket()
503 exiting = True
474 exiting = True
504 ready = selector.select(timeout=h.pollinterval)
475 ready = selector.select(timeout=h.pollinterval)
505 if not ready:
476 if not ready:
506 # only exit if we completed all queued requests
477 # only exit if we completed all queued requests
507 if exiting:
478 if exiting:
508 break
479 break
509 continue
480 continue
510 try:
481 try:
511 conn, _addr = self._sock.accept()
482 conn, _addr = self._sock.accept()
512 except socket.error as inst:
483 except socket.error as inst:
513 if inst.args[0] == errno.EINTR:
484 if inst.args[0] == errno.EINTR:
514 continue
485 continue
515 raise
486 raise
516
487
517 pid = os.fork()
488 pid = os.fork()
518 if pid:
489 if pid:
519 try:
490 try:
520 self.ui.debug('forked worker process (pid=%d)\n' % pid)
491 self.ui.debug('forked worker process (pid=%d)\n' % pid)
521 self._workerpids.add(pid)
492 self._workerpids.add(pid)
522 h.newconnection()
493 h.newconnection()
523 finally:
494 finally:
524 conn.close() # release handle in parent process
495 conn.close() # release handle in parent process
525 else:
496 else:
526 try:
497 try:
527 self._runworker(conn)
498 self._runworker(conn)
528 conn.close()
499 conn.close()
529 os._exit(0)
500 os._exit(0)
530 except: # never return, hence no re-raises
501 except: # never return, hence no re-raises
531 try:
502 try:
532 self.ui.traceback(force=True)
503 self.ui.traceback(force=True)
533 finally:
504 finally:
534 os._exit(255)
505 os._exit(255)
535 selector.close()
506 selector.close()
536
507
537 def _sigchldhandler(self, signal, frame):
508 def _sigchldhandler(self, signal, frame):
538 self._reapworkers(os.WNOHANG)
509 self._reapworkers(os.WNOHANG)
539
510
540 def _reapworkers(self, options):
511 def _reapworkers(self, options):
541 while self._workerpids:
512 while self._workerpids:
542 try:
513 try:
543 pid, _status = os.waitpid(-1, options)
514 pid, _status = os.waitpid(-1, options)
544 except OSError as inst:
515 except OSError as inst:
545 if inst.errno == errno.EINTR:
516 if inst.errno == errno.EINTR:
546 continue
517 continue
547 if inst.errno != errno.ECHILD:
518 if inst.errno != errno.ECHILD:
548 raise
519 raise
549 # no child processes at all (reaped by other waitpid()?)
520 # no child processes at all (reaped by other waitpid()?)
550 self._workerpids.clear()
521 self._workerpids.clear()
551 return
522 return
552 if pid == 0:
523 if pid == 0:
553 # no waitable child processes
524 # no waitable child processes
554 return
525 return
555 self.ui.debug('worker process exited (pid=%d)\n' % pid)
526 self.ui.debug('worker process exited (pid=%d)\n' % pid)
556 self._workerpids.discard(pid)
527 self._workerpids.discard(pid)
557
528
558 def _runworker(self, conn):
529 def _runworker(self, conn):
559 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
530 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
560 _initworkerprocess()
531 _initworkerprocess()
561 h = self._servicehandler
532 h = self._servicehandler
562 try:
533 try:
563 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
534 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
564 finally:
535 finally:
565 gc.collect() # trigger __del__ since worker process uses os._exit
536 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,319 +1,348 b''
1 # procutil.py - utility for managing processes and executable environment
1 # procutil.py - utility for managing processes and executable environment
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import imp
12 import imp
13 import io
13 import io
14 import os
14 import os
15 import signal
15 import signal
16 import subprocess
16 import subprocess
17 import sys
17 import sys
18 import tempfile
18 import tempfile
19 import time
19 import time
20
20
21 from ..i18n import _
21 from ..i18n import _
22
22
23 from .. import (
23 from .. import (
24 encoding,
24 encoding,
25 error,
25 error,
26 policy,
26 policy,
27 pycompat,
27 pycompat,
28 )
28 )
29
29
30 osutil = policy.importmod(r'osutil')
30 osutil = policy.importmod(r'osutil')
31
31
32 stderr = pycompat.stderr
32 stderr = pycompat.stderr
33 stdin = pycompat.stdin
33 stdin = pycompat.stdin
34 stdout = pycompat.stdout
34 stdout = pycompat.stdout
35
35
36 def isatty(fp):
36 def isatty(fp):
37 try:
37 try:
38 return fp.isatty()
38 return fp.isatty()
39 except AttributeError:
39 except AttributeError:
40 return False
40 return False
41
41
42 # glibc determines buffering on first write to stdout - if we replace a TTY
42 # glibc determines buffering on first write to stdout - if we replace a TTY
43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
44 # buffering
44 # buffering
45 if isatty(stdout):
45 if isatty(stdout):
46 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
46 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
47
47
48 if pycompat.iswindows:
48 if pycompat.iswindows:
49 from .. import windows as platform
49 from .. import windows as platform
50 stdout = platform.winstdout(stdout)
50 stdout = platform.winstdout(stdout)
51 else:
51 else:
52 from .. import posix as platform
52 from .. import posix as platform
53
53
54 explainexit = platform.explainexit
54 explainexit = platform.explainexit
55 findexe = platform.findexe
55 findexe = platform.findexe
56 _gethgcmd = platform.gethgcmd
56 _gethgcmd = platform.gethgcmd
57 getuser = platform.getuser
57 getuser = platform.getuser
58 getpid = os.getpid
58 getpid = os.getpid
59 hidewindow = platform.hidewindow
59 hidewindow = platform.hidewindow
60 popen = platform.popen
60 popen = platform.popen
61 quotecommand = platform.quotecommand
61 quotecommand = platform.quotecommand
62 readpipe = platform.readpipe
62 readpipe = platform.readpipe
63 setbinary = platform.setbinary
63 setbinary = platform.setbinary
64 setsignalhandler = platform.setsignalhandler
64 setsignalhandler = platform.setsignalhandler
65 shellquote = platform.shellquote
65 shellquote = platform.shellquote
66 shellsplit = platform.shellsplit
66 shellsplit = platform.shellsplit
67 spawndetached = platform.spawndetached
67 spawndetached = platform.spawndetached
68 sshargs = platform.sshargs
68 sshargs = platform.sshargs
69 testpid = platform.testpid
69 testpid = platform.testpid
70
70
71 try:
71 try:
72 setprocname = osutil.setprocname
72 setprocname = osutil.setprocname
73 except AttributeError:
73 except AttributeError:
74 pass
74 pass
75 try:
75 try:
76 unblocksignal = osutil.unblocksignal
76 unblocksignal = osutil.unblocksignal
77 except AttributeError:
77 except AttributeError:
78 pass
78 pass
79
79
80 closefds = pycompat.isposix
80 closefds = pycompat.isposix
81
81
82 def popen2(cmd, env=None, newlines=False):
82 def popen2(cmd, env=None, newlines=False):
83 # Setting bufsize to -1 lets the system decide the buffer size.
83 # Setting bufsize to -1 lets the system decide the buffer size.
84 # The default for bufsize is 0, meaning unbuffered. This leads to
84 # The default for bufsize is 0, meaning unbuffered. This leads to
85 # poor performance on Mac OS X: http://bugs.python.org/issue4194
85 # poor performance on Mac OS X: http://bugs.python.org/issue4194
86 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
86 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
87 close_fds=closefds,
87 close_fds=closefds,
88 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
88 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
89 universal_newlines=newlines,
89 universal_newlines=newlines,
90 env=env)
90 env=env)
91 return p.stdin, p.stdout
91 return p.stdin, p.stdout
92
92
93 def popen3(cmd, env=None, newlines=False):
93 def popen3(cmd, env=None, newlines=False):
94 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
94 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
95 return stdin, stdout, stderr
95 return stdin, stdout, stderr
96
96
97 def popen4(cmd, env=None, newlines=False, bufsize=-1):
97 def popen4(cmd, env=None, newlines=False, bufsize=-1):
98 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
98 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
99 close_fds=closefds,
99 close_fds=closefds,
100 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
100 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
101 stderr=subprocess.PIPE,
101 stderr=subprocess.PIPE,
102 universal_newlines=newlines,
102 universal_newlines=newlines,
103 env=env)
103 env=env)
104 return p.stdin, p.stdout, p.stderr, p
104 return p.stdin, p.stdout, p.stderr, p
105
105
106 def pipefilter(s, cmd):
106 def pipefilter(s, cmd):
107 '''filter string S through command CMD, returning its output'''
107 '''filter string S through command CMD, returning its output'''
108 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
108 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
109 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
109 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
110 pout, perr = p.communicate(s)
110 pout, perr = p.communicate(s)
111 return pout
111 return pout
112
112
113 def tempfilter(s, cmd):
113 def tempfilter(s, cmd):
114 '''filter string S through a pair of temporary files with CMD.
114 '''filter string S through a pair of temporary files with CMD.
115 CMD is used as a template to create the real command to be run,
115 CMD is used as a template to create the real command to be run,
116 with the strings INFILE and OUTFILE replaced by the real names of
116 with the strings INFILE and OUTFILE replaced by the real names of
117 the temporary files generated.'''
117 the temporary files generated.'''
118 inname, outname = None, None
118 inname, outname = None, None
119 try:
119 try:
120 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
120 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
121 fp = os.fdopen(infd, r'wb')
121 fp = os.fdopen(infd, r'wb')
122 fp.write(s)
122 fp.write(s)
123 fp.close()
123 fp.close()
124 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
124 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
125 os.close(outfd)
125 os.close(outfd)
126 cmd = cmd.replace('INFILE', inname)
126 cmd = cmd.replace('INFILE', inname)
127 cmd = cmd.replace('OUTFILE', outname)
127 cmd = cmd.replace('OUTFILE', outname)
128 code = os.system(cmd)
128 code = os.system(cmd)
129 if pycompat.sysplatform == 'OpenVMS' and code & 1:
129 if pycompat.sysplatform == 'OpenVMS' and code & 1:
130 code = 0
130 code = 0
131 if code:
131 if code:
132 raise error.Abort(_("command '%s' failed: %s") %
132 raise error.Abort(_("command '%s' failed: %s") %
133 (cmd, explainexit(code)))
133 (cmd, explainexit(code)))
134 with open(outname, 'rb') as fp:
134 with open(outname, 'rb') as fp:
135 return fp.read()
135 return fp.read()
136 finally:
136 finally:
137 try:
137 try:
138 if inname:
138 if inname:
139 os.unlink(inname)
139 os.unlink(inname)
140 except OSError:
140 except OSError:
141 pass
141 pass
142 try:
142 try:
143 if outname:
143 if outname:
144 os.unlink(outname)
144 os.unlink(outname)
145 except OSError:
145 except OSError:
146 pass
146 pass
147
147
148 _filtertable = {
148 _filtertable = {
149 'tempfile:': tempfilter,
149 'tempfile:': tempfilter,
150 'pipe:': pipefilter,
150 'pipe:': pipefilter,
151 }
151 }
152
152
153 def filter(s, cmd):
153 def filter(s, cmd):
154 "filter a string through a command that transforms its input to its output"
154 "filter a string through a command that transforms its input to its output"
155 for name, fn in _filtertable.iteritems():
155 for name, fn in _filtertable.iteritems():
156 if cmd.startswith(name):
156 if cmd.startswith(name):
157 return fn(s, cmd[len(name):].lstrip())
157 return fn(s, cmd[len(name):].lstrip())
158 return pipefilter(s, cmd)
158 return pipefilter(s, cmd)
159
159
160 def mainfrozen():
160 def mainfrozen():
161 """return True if we are a frozen executable.
161 """return True if we are a frozen executable.
162
162
163 The code supports py2exe (most common, Windows only) and tools/freeze
163 The code supports py2exe (most common, Windows only) and tools/freeze
164 (portable, not much used).
164 (portable, not much used).
165 """
165 """
166 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
166 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
167 pycompat.safehasattr(sys, "importers") or # old py2exe
167 pycompat.safehasattr(sys, "importers") or # old py2exe
168 imp.is_frozen(u"__main__")) # tools/freeze
168 imp.is_frozen(u"__main__")) # tools/freeze
169
169
170 _hgexecutable = None
170 _hgexecutable = None
171
171
172 def hgexecutable():
172 def hgexecutable():
173 """return location of the 'hg' executable.
173 """return location of the 'hg' executable.
174
174
175 Defaults to $HG or 'hg' in the search path.
175 Defaults to $HG or 'hg' in the search path.
176 """
176 """
177 if _hgexecutable is None:
177 if _hgexecutable is None:
178 hg = encoding.environ.get('HG')
178 hg = encoding.environ.get('HG')
179 mainmod = sys.modules[r'__main__']
179 mainmod = sys.modules[r'__main__']
180 if hg:
180 if hg:
181 _sethgexecutable(hg)
181 _sethgexecutable(hg)
182 elif mainfrozen():
182 elif mainfrozen():
183 if getattr(sys, 'frozen', None) == 'macosx_app':
183 if getattr(sys, 'frozen', None) == 'macosx_app':
184 # Env variable set by py2app
184 # Env variable set by py2app
185 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
185 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
186 else:
186 else:
187 _sethgexecutable(pycompat.sysexecutable)
187 _sethgexecutable(pycompat.sysexecutable)
188 elif (os.path.basename(
188 elif (os.path.basename(
189 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
189 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
190 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
190 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
191 else:
191 else:
192 exe = findexe('hg') or os.path.basename(sys.argv[0])
192 exe = findexe('hg') or os.path.basename(sys.argv[0])
193 _sethgexecutable(exe)
193 _sethgexecutable(exe)
194 return _hgexecutable
194 return _hgexecutable
195
195
196 def _sethgexecutable(path):
196 def _sethgexecutable(path):
197 """set location of the 'hg' executable"""
197 """set location of the 'hg' executable"""
198 global _hgexecutable
198 global _hgexecutable
199 _hgexecutable = path
199 _hgexecutable = path
200
200
201 def _testfileno(f, stdf):
201 def _testfileno(f, stdf):
202 fileno = getattr(f, 'fileno', None)
202 fileno = getattr(f, 'fileno', None)
203 try:
203 try:
204 return fileno and fileno() == stdf.fileno()
204 return fileno and fileno() == stdf.fileno()
205 except io.UnsupportedOperation:
205 except io.UnsupportedOperation:
206 return False # fileno() raised UnsupportedOperation
206 return False # fileno() raised UnsupportedOperation
207
207
208 def isstdin(f):
208 def isstdin(f):
209 return _testfileno(f, sys.__stdin__)
209 return _testfileno(f, sys.__stdin__)
210
210
211 def isstdout(f):
211 def isstdout(f):
212 return _testfileno(f, sys.__stdout__)
212 return _testfileno(f, sys.__stdout__)
213
213
214 def protectstdio(uin, uout):
215 """Duplicate streams and redirect original to null if (uin, uout) are
216 stdio
217
218 Returns (fin, fout) which point to the original (uin, uout) fds, but
219 may be copy of (uin, uout). The returned streams can be considered
220 "owned" in that print(), exec(), etc. never reach to them.
221 """
222 uout.flush()
223 newfiles = []
224 nullfd = os.open(os.devnull, os.O_RDWR)
225 for f, sysf, mode in [(uin, stdin, r'rb'),
226 (uout, stdout, r'wb')]:
227 if f is sysf:
228 newfd = os.dup(f.fileno())
229 os.dup2(nullfd, f.fileno())
230 f = os.fdopen(newfd, mode)
231 newfiles.append(f)
232 os.close(nullfd)
233 return tuple(newfiles)
234
235 def restorestdio(uin, uout, fin, fout):
236 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
237 uout.flush()
238 for f, uif in [(fin, uin), (fout, uout)]:
239 if f is not uif:
240 os.dup2(f.fileno(), uif.fileno())
241 f.close()
242
214 def shellenviron(environ=None):
243 def shellenviron(environ=None):
215 """return environ with optional override, useful for shelling out"""
244 """return environ with optional override, useful for shelling out"""
216 def py2shell(val):
245 def py2shell(val):
217 'convert python object into string that is useful to shell'
246 'convert python object into string that is useful to shell'
218 if val is None or val is False:
247 if val is None or val is False:
219 return '0'
248 return '0'
220 if val is True:
249 if val is True:
221 return '1'
250 return '1'
222 return pycompat.bytestr(val)
251 return pycompat.bytestr(val)
223 env = dict(encoding.environ)
252 env = dict(encoding.environ)
224 if environ:
253 if environ:
225 env.update((k, py2shell(v)) for k, v in environ.iteritems())
254 env.update((k, py2shell(v)) for k, v in environ.iteritems())
226 env['HG'] = hgexecutable()
255 env['HG'] = hgexecutable()
227 return env
256 return env
228
257
229 def system(cmd, environ=None, cwd=None, out=None):
258 def system(cmd, environ=None, cwd=None, out=None):
230 '''enhanced shell command execution.
259 '''enhanced shell command execution.
231 run with environment maybe modified, maybe in different dir.
260 run with environment maybe modified, maybe in different dir.
232
261
233 if out is specified, it is assumed to be a file-like object that has a
262 if out is specified, it is assumed to be a file-like object that has a
234 write() method. stdout and stderr will be redirected to out.'''
263 write() method. stdout and stderr will be redirected to out.'''
235 try:
264 try:
236 stdout.flush()
265 stdout.flush()
237 except Exception:
266 except Exception:
238 pass
267 pass
239 cmd = quotecommand(cmd)
268 cmd = quotecommand(cmd)
240 env = shellenviron(environ)
269 env = shellenviron(environ)
241 if out is None or isstdout(out):
270 if out is None or isstdout(out):
242 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
271 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
243 env=env, cwd=cwd)
272 env=env, cwd=cwd)
244 else:
273 else:
245 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
274 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
246 env=env, cwd=cwd, stdout=subprocess.PIPE,
275 env=env, cwd=cwd, stdout=subprocess.PIPE,
247 stderr=subprocess.STDOUT)
276 stderr=subprocess.STDOUT)
248 for line in iter(proc.stdout.readline, ''):
277 for line in iter(proc.stdout.readline, ''):
249 out.write(line)
278 out.write(line)
250 proc.wait()
279 proc.wait()
251 rc = proc.returncode
280 rc = proc.returncode
252 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
281 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
253 rc = 0
282 rc = 0
254 return rc
283 return rc
255
284
256 def gui():
285 def gui():
257 '''Are we running in a GUI?'''
286 '''Are we running in a GUI?'''
258 if pycompat.isdarwin:
287 if pycompat.isdarwin:
259 if 'SSH_CONNECTION' in encoding.environ:
288 if 'SSH_CONNECTION' in encoding.environ:
260 # handle SSH access to a box where the user is logged in
289 # handle SSH access to a box where the user is logged in
261 return False
290 return False
262 elif getattr(osutil, 'isgui', None):
291 elif getattr(osutil, 'isgui', None):
263 # check if a CoreGraphics session is available
292 # check if a CoreGraphics session is available
264 return osutil.isgui()
293 return osutil.isgui()
265 else:
294 else:
266 # pure build; use a safe default
295 # pure build; use a safe default
267 return True
296 return True
268 else:
297 else:
269 return pycompat.iswindows or encoding.environ.get("DISPLAY")
298 return pycompat.iswindows or encoding.environ.get("DISPLAY")
270
299
271 def hgcmd():
300 def hgcmd():
272 """Return the command used to execute current hg
301 """Return the command used to execute current hg
273
302
274 This is different from hgexecutable() because on Windows we want
303 This is different from hgexecutable() because on Windows we want
275 to avoid things opening new shell windows like batch files, so we
304 to avoid things opening new shell windows like batch files, so we
276 get either the python call or current executable.
305 get either the python call or current executable.
277 """
306 """
278 if mainfrozen():
307 if mainfrozen():
279 if getattr(sys, 'frozen', None) == 'macosx_app':
308 if getattr(sys, 'frozen', None) == 'macosx_app':
280 # Env variable set by py2app
309 # Env variable set by py2app
281 return [encoding.environ['EXECUTABLEPATH']]
310 return [encoding.environ['EXECUTABLEPATH']]
282 else:
311 else:
283 return [pycompat.sysexecutable]
312 return [pycompat.sysexecutable]
284 return _gethgcmd()
313 return _gethgcmd()
285
314
286 def rundetached(args, condfn):
315 def rundetached(args, condfn):
287 """Execute the argument list in a detached process.
316 """Execute the argument list in a detached process.
288
317
289 condfn is a callable which is called repeatedly and should return
318 condfn is a callable which is called repeatedly and should return
290 True once the child process is known to have started successfully.
319 True once the child process is known to have started successfully.
291 At this point, the child process PID is returned. If the child
320 At this point, the child process PID is returned. If the child
292 process fails to start or finishes before condfn() evaluates to
321 process fails to start or finishes before condfn() evaluates to
293 True, return -1.
322 True, return -1.
294 """
323 """
295 # Windows case is easier because the child process is either
324 # Windows case is easier because the child process is either
296 # successfully starting and validating the condition or exiting
325 # successfully starting and validating the condition or exiting
297 # on failure. We just poll on its PID. On Unix, if the child
326 # on failure. We just poll on its PID. On Unix, if the child
298 # process fails to start, it will be left in a zombie state until
327 # process fails to start, it will be left in a zombie state until
299 # the parent wait on it, which we cannot do since we expect a long
328 # the parent wait on it, which we cannot do since we expect a long
300 # running process on success. Instead we listen for SIGCHLD telling
329 # running process on success. Instead we listen for SIGCHLD telling
301 # us our child process terminated.
330 # us our child process terminated.
302 terminated = set()
331 terminated = set()
303 def handler(signum, frame):
332 def handler(signum, frame):
304 terminated.add(os.wait())
333 terminated.add(os.wait())
305 prevhandler = None
334 prevhandler = None
306 SIGCHLD = getattr(signal, 'SIGCHLD', None)
335 SIGCHLD = getattr(signal, 'SIGCHLD', None)
307 if SIGCHLD is not None:
336 if SIGCHLD is not None:
308 prevhandler = signal.signal(SIGCHLD, handler)
337 prevhandler = signal.signal(SIGCHLD, handler)
309 try:
338 try:
310 pid = spawndetached(args)
339 pid = spawndetached(args)
311 while not condfn():
340 while not condfn():
312 if ((pid in terminated or not testpid(pid))
341 if ((pid in terminated or not testpid(pid))
313 and not condfn()):
342 and not condfn()):
314 return -1
343 return -1
315 time.sleep(0.1)
344 time.sleep(0.1)
316 return pid
345 return pid
317 finally:
346 finally:
318 if prevhandler is not None:
347 if prevhandler is not None:
319 signal.signal(signal.SIGCHLD, prevhandler)
348 signal.signal(signal.SIGCHLD, prevhandler)
General Comments 0
You need to be logged in to leave comments. Login now