##// END OF EJS Templates
chgserver: remove Python 2 file descriptor logic...
Yuya Nishihara -
r49806:cf99c4af default
parent child Browse files
Show More
@@ -1,755 +1,749
1 1 # chgserver.py - command server extension for cHg
2 2 #
3 3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """command server extension for cHg
9 9
10 10 'S' channel (read/write)
11 11 propagate ui.system() request to client
12 12
13 13 'attachio' command
14 14 attach client's stdio passed by sendmsg()
15 15
16 16 'chdir' command
17 17 change current directory
18 18
19 19 'setenv' command
20 20 replace os.environ completely
21 21
22 22 'setumask' command (DEPRECATED)
23 23 'setumask2' command
24 24 set umask
25 25
26 26 'validate' command
27 27 reload the config and check if the server is up to date
28 28
29 29 Config
30 30 ------
31 31
32 32 ::
33 33
34 34 [chgserver]
35 35 # how long (in seconds) should an idle chg server exit
36 36 idletimeout = 3600
37 37
38 38 # whether to skip config or env change checks
39 39 skiphash = False
40 40 """
41 41
42 42
43 43 import inspect
44 44 import os
45 45 import re
46 46 import socket
47 47 import stat
48 48 import struct
49 49 import time
50 50
51 51 from .i18n import _
52 52 from .pycompat import (
53 53 getattr,
54 54 setattr,
55 55 )
56 56 from .node import hex
57 57
58 58 from . import (
59 59 commandserver,
60 60 encoding,
61 61 error,
62 62 extensions,
63 63 pycompat,
64 64 util,
65 65 )
66 66
67 67 from .utils import (
68 68 hashutil,
69 69 procutil,
70 70 stringutil,
71 71 )
72 72
73 73
74 74 def _hashlist(items):
75 75 """return sha1 hexdigest for a list"""
76 76 return hex(hashutil.sha1(stringutil.pprint(items)).digest())
77 77
78 78
79 79 # sensitive config sections affecting confighash
80 80 _configsections = [
81 81 b'alias', # affects global state commands.table
82 82 b'diff-tools', # affects whether gui or not in extdiff's uisetup
83 83 b'eol', # uses setconfig('eol', ...)
84 84 b'extdiff', # uisetup will register new commands
85 85 b'extensions',
86 86 b'fastannotate', # affects annotate command and adds fastannonate cmd
87 87 b'merge-tools', # affects whether gui or not in extdiff's uisetup
88 88 b'schemes', # extsetup will update global hg.schemes
89 89 ]
90 90
91 91 _configsectionitems = [
92 92 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
93 93 ]
94 94
95 95 # sensitive environment variables affecting confighash
96 96 _envre = re.compile(
97 97 br'''\A(?:
98 98 CHGHG
99 99 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
100 100 |HG(?:ENCODING|PLAIN).*
101 101 |LANG(?:UAGE)?
102 102 |LC_.*
103 103 |LD_.*
104 104 |PATH
105 105 |PYTHON.*
106 106 |TERM(?:INFO)?
107 107 |TZ
108 108 )\Z''',
109 109 re.X,
110 110 )
111 111
112 112
113 113 def _confighash(ui):
114 114 """return a quick hash for detecting config/env changes
115 115
116 116 confighash is the hash of sensitive config items and environment variables.
117 117
118 118 for chgserver, it is designed that once confighash changes, the server is
119 119 not qualified to serve its client and should redirect the client to a new
120 120 server. different from mtimehash, confighash change will not mark the
121 121 server outdated and exit since the user can have different configs at the
122 122 same time.
123 123 """
124 124 sectionitems = []
125 125 for section in _configsections:
126 126 sectionitems.append(ui.configitems(section))
127 127 for section, item in _configsectionitems:
128 128 sectionitems.append(ui.config(section, item))
129 129 sectionhash = _hashlist(sectionitems)
130 130 # If $CHGHG is set, the change to $HG should not trigger a new chg server
131 131 if b'CHGHG' in encoding.environ:
132 132 ignored = {b'HG'}
133 133 else:
134 134 ignored = set()
135 135 envitems = [
136 136 (k, v)
137 137 for k, v in encoding.environ.items()
138 138 if _envre.match(k) and k not in ignored
139 139 ]
140 140 envhash = _hashlist(sorted(envitems))
141 141 return sectionhash[:6] + envhash[:6]
142 142
143 143
144 144 def _getmtimepaths(ui):
145 145 """get a list of paths that should be checked to detect change
146 146
147 147 The list will include:
148 148 - extensions (will not cover all files for complex extensions)
149 149 - mercurial/__version__.py
150 150 - python binary
151 151 """
152 152 modules = [m for n, m in extensions.extensions(ui)]
153 153 try:
154 154 from . import __version__
155 155
156 156 modules.append(__version__)
157 157 except ImportError:
158 158 pass
159 159 files = []
160 160 if pycompat.sysexecutable:
161 161 files.append(pycompat.sysexecutable)
162 162 for m in modules:
163 163 try:
164 164 files.append(pycompat.fsencode(inspect.getabsfile(m)))
165 165 except TypeError:
166 166 pass
167 167 return sorted(set(files))
168 168
169 169
170 170 def _mtimehash(paths):
171 171 """return a quick hash for detecting file changes
172 172
173 173 mtimehash calls stat on given paths and calculate a hash based on size and
174 174 mtime of each file. mtimehash does not read file content because reading is
175 175 expensive. therefore it's not 100% reliable for detecting content changes.
176 176 it's possible to return different hashes for same file contents.
177 177 it's also possible to return a same hash for different file contents for
178 178 some carefully crafted situation.
179 179
180 180 for chgserver, it is designed that once mtimehash changes, the server is
181 181 considered outdated immediately and should no longer provide service.
182 182
183 183 mtimehash is not included in confighash because we only know the paths of
184 184 extensions after importing them (there is imp.find_module but that faces
185 185 race conditions). We need to calculate confighash without importing.
186 186 """
187 187
188 188 def trystat(path):
189 189 try:
190 190 st = os.stat(path)
191 191 return (st[stat.ST_MTIME], st.st_size)
192 192 except OSError:
193 193 # could be ENOENT, EPERM etc. not fatal in any case
194 194 pass
195 195
196 196 return _hashlist(pycompat.maplist(trystat, paths))[:12]
197 197
198 198
199 199 class hashstate:
200 200 """a structure storing confighash, mtimehash, paths used for mtimehash"""
201 201
202 202 def __init__(self, confighash, mtimehash, mtimepaths):
203 203 self.confighash = confighash
204 204 self.mtimehash = mtimehash
205 205 self.mtimepaths = mtimepaths
206 206
207 207 @staticmethod
208 208 def fromui(ui, mtimepaths=None):
209 209 if mtimepaths is None:
210 210 mtimepaths = _getmtimepaths(ui)
211 211 confighash = _confighash(ui)
212 212 mtimehash = _mtimehash(mtimepaths)
213 213 ui.log(
214 214 b'cmdserver',
215 215 b'confighash = %s mtimehash = %s\n',
216 216 confighash,
217 217 mtimehash,
218 218 )
219 219 return hashstate(confighash, mtimehash, mtimepaths)
220 220
221 221
222 222 def _newchgui(srcui, csystem, attachio):
223 223 class chgui(srcui.__class__):
224 224 def __init__(self, src=None):
225 225 super(chgui, self).__init__(src)
226 226 if src:
227 227 self._csystem = getattr(src, '_csystem', csystem)
228 228 else:
229 229 self._csystem = csystem
230 230
231 231 def _runsystem(self, cmd, environ, cwd, out):
232 232 # fallback to the original system method if
233 233 # a. the output stream is not stdout (e.g. stderr, cStringIO),
234 234 # b. or stdout is redirected by protectfinout(),
235 235 # because the chg client is not aware of these situations and
236 236 # will behave differently (i.e. write to stdout).
237 237 if (
238 238 out is not self.fout
239 239 or not util.safehasattr(self.fout, b'fileno')
240 240 or self.fout.fileno() != procutil.stdout.fileno()
241 241 or self._finoutredirected
242 242 ):
243 243 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
244 244 self.flush()
245 245 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
246 246
247 247 def _runpager(self, cmd, env=None):
248 248 self._csystem(
249 249 cmd,
250 250 procutil.shellenviron(env),
251 251 type=b'pager',
252 252 cmdtable={b'attachio': attachio},
253 253 )
254 254 return True
255 255
256 256 return chgui(srcui)
257 257
258 258
259 259 def _loadnewui(srcui, args, cdebug):
260 260 from . import dispatch # avoid cycle
261 261
262 262 newui = srcui.__class__.load()
263 263 for a in [b'fin', b'fout', b'ferr', b'environ']:
264 264 setattr(newui, a, getattr(srcui, a))
265 265 if util.safehasattr(srcui, b'_csystem'):
266 266 newui._csystem = srcui._csystem
267 267
268 268 # command line args
269 269 options = dispatch._earlyparseopts(newui, args)
270 270 dispatch._parseconfig(newui, options[b'config'])
271 271
272 272 # stolen from tortoisehg.util.copydynamicconfig()
273 273 for section, name, value in srcui.walkconfig():
274 274 source = srcui.configsource(section, name)
275 275 if b':' in source or source == b'--config' or source.startswith(b'$'):
276 276 # path:line or command line, or environ
277 277 continue
278 278 newui.setconfig(section, name, value, source)
279 279
280 280 # load wd and repo config, copied from dispatch.py
281 281 cwd = options[b'cwd']
282 282 cwd = cwd and os.path.realpath(cwd) or None
283 283 rpath = options[b'repository']
284 284 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
285 285
286 286 extensions.populateui(newui)
287 287 commandserver.setuplogging(newui, fp=cdebug)
288 288 if newui is not newlui:
289 289 extensions.populateui(newlui)
290 290 commandserver.setuplogging(newlui, fp=cdebug)
291 291
292 292 return (newui, newlui)
293 293
294 294
295 295 class channeledsystem:
296 296 """Propagate ui.system() request in the following format:
297 297
298 298 payload length (unsigned int),
299 299 type, '\0',
300 300 cmd, '\0',
301 301 cwd, '\0',
302 302 envkey, '=', val, '\0',
303 303 ...
304 304 envkey, '=', val
305 305
306 306 if type == 'system', waits for:
307 307
308 308 exitcode length (unsigned int),
309 309 exitcode (int)
310 310
311 311 if type == 'pager', repetitively waits for a command name ending with '\n'
312 312 and executes it defined by cmdtable, or exits the loop if the command name
313 313 is empty.
314 314 """
315 315
316 316 def __init__(self, in_, out, channel):
317 317 self.in_ = in_
318 318 self.out = out
319 319 self.channel = channel
320 320
321 321 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
322 322 args = [type, cmd, util.abspath(cwd or b'.')]
323 323 args.extend(b'%s=%s' % (k, v) for k, v in environ.items())
324 324 data = b'\0'.join(args)
325 325 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
326 326 self.out.write(data)
327 327 self.out.flush()
328 328
329 329 if type == b'system':
330 330 length = self.in_.read(4)
331 331 (length,) = struct.unpack(b'>I', length)
332 332 if length != 4:
333 333 raise error.Abort(_(b'invalid response'))
334 334 (rc,) = struct.unpack(b'>i', self.in_.read(4))
335 335 return rc
336 336 elif type == b'pager':
337 337 while True:
338 338 cmd = self.in_.readline()[:-1]
339 339 if not cmd:
340 340 break
341 341 if cmdtable and cmd in cmdtable:
342 342 cmdtable[cmd]()
343 343 else:
344 344 raise error.Abort(_(b'unexpected command: %s') % cmd)
345 345 else:
346 346 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
347 347
348 348
349 349 _iochannels = [
350 350 # server.ch, ui.fp, mode
351 351 (b'cin', b'fin', 'rb'),
352 352 (b'cout', b'fout', 'wb'),
353 353 (b'cerr', b'ferr', 'wb'),
354 354 ]
355 355
356 356
357 357 class chgcmdserver(commandserver.server):
358 358 def __init__(
359 359 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
360 360 ):
361 361 super(chgcmdserver, self).__init__(
362 362 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
363 363 repo,
364 364 fin,
365 365 fout,
366 366 prereposetups,
367 367 )
368 368 self.clientsock = sock
369 369 self._ioattached = False
370 370 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
371 371 self.hashstate = hashstate
372 372 self.baseaddress = baseaddress
373 373 if hashstate is not None:
374 374 self.capabilities = self.capabilities.copy()
375 375 self.capabilities[b'validate'] = chgcmdserver.validate
376 376
377 377 def cleanup(self):
378 378 super(chgcmdserver, self).cleanup()
379 379 # dispatch._runcatch() does not flush outputs if exception is not
380 380 # handled by dispatch._dispatch()
381 381 self.ui.flush()
382 382 self._restoreio()
383 383 self._ioattached = False
384 384
385 385 def attachio(self):
386 386 """Attach to client's stdio passed via unix domain socket; all
387 387 channels except cresult will no longer be used
388 388 """
389 389 # tell client to sendmsg() with 1-byte payload, which makes it
390 390 # distinctive from "attachio\n" command consumed by client.read()
391 391 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
392 392 clientfds = util.recvfds(self.clientsock.fileno())
393 393 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
394 394
395 395 ui = self.ui
396 396 ui.flush()
397 397 self._saveio()
398 398 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
399 399 assert fd > 0
400 400 fp = getattr(ui, fn)
401 401 os.dup2(fd, fp.fileno())
402 402 os.close(fd)
403 403 if self._ioattached:
404 404 continue
405 405 # reset buffering mode when client is first attached. as we want
406 406 # to see output immediately on pager, the mode stays unchanged
407 407 # when client re-attached. ferr is unchanged because it should
408 408 # be unbuffered no matter if it is a tty or not.
409 409 if fn == b'ferr':
410 410 newfp = fp
411 411 else:
412 412 # On Python 3, the standard library doesn't offer line-buffered
413 413 # binary streams, so wrap/unwrap it.
414 414 if fp.isatty():
415 415 newfp = procutil.make_line_buffered(fp)
416 416 else:
417 417 newfp = procutil.unwrap_line_buffered(fp)
418 418 if newfp is not fp:
419 419 setattr(ui, fn, newfp)
420 420 setattr(self, cn, newfp)
421 421
422 422 self._ioattached = True
423 423 self.cresult.write(struct.pack(b'>i', len(clientfds)))
424 424
425 425 def _saveio(self):
426 426 if self._oldios:
427 427 return
428 428 ui = self.ui
429 429 for cn, fn, _mode in _iochannels:
430 430 ch = getattr(self, cn)
431 431 fp = getattr(ui, fn)
432 432 fd = os.dup(fp.fileno())
433 433 self._oldios.append((ch, fp, fd))
434 434
435 435 def _restoreio(self):
436 436 if not self._oldios:
437 437 return
438 438 nullfd = os.open(os.devnull, os.O_WRONLY)
439 439 ui = self.ui
440 440 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
441 newfp = getattr(ui, fn)
442 # On Python 3, newfp is just a wrapper around fp even if newfp is
443 # not fp, so deleting newfp is safe.
444 if newfp is not fp:
445 newfp.close()
446 # restore original fd: fp is open again
447 441 try:
448 if newfp is fp and 'w' in mode:
442 if 'w' in mode:
449 443 # Discard buffered data which couldn't be flushed because
450 444 # of EPIPE. The data should belong to the current session
451 445 # and should never persist.
452 446 os.dup2(nullfd, fp.fileno())
453 447 fp.flush()
454 448 os.dup2(fd, fp.fileno())
455 449 except OSError as err:
456 450 # According to issue6330, running chg on heavy loaded systems
457 451 # can lead to EBUSY. [man dup2] indicates that, on Linux,
458 452 # EBUSY comes from a race condition between open() and dup2().
459 453 # However it's not clear why open() race occurred for
460 454 # newfd=stdin/out/err.
461 455 self.ui.log(
462 456 b'chgserver',
463 457 b'got %s while duplicating %s\n',
464 458 stringutil.forcebytestr(err),
465 459 fn,
466 460 )
467 461 os.close(fd)
468 462 setattr(self, cn, ch)
469 463 setattr(ui, fn, fp)
470 464 os.close(nullfd)
471 465 del self._oldios[:]
472 466
473 467 def validate(self):
474 468 """Reload the config and check if the server is up to date
475 469
476 470 Read a list of '\0' separated arguments.
477 471 Write a non-empty list of '\0' separated instruction strings or '\0'
478 472 if the list is empty.
479 473 An instruction string could be either:
480 474 - "unlink $path", the client should unlink the path to stop the
481 475 outdated server.
482 476 - "redirect $path", the client should attempt to connect to $path
483 477 first. If it does not work, start a new server. It implies
484 478 "reconnect".
485 479 - "exit $n", the client should exit directly with code n.
486 480 This may happen if we cannot parse the config.
487 481 - "reconnect", the client should close the connection and
488 482 reconnect.
489 483 If neither "reconnect" nor "redirect" is included in the instruction
490 484 list, the client can continue with this server after completing all
491 485 the instructions.
492 486 """
493 487 args = self._readlist()
494 488 errorraised = False
495 489 detailed_exit_code = 255
496 490 try:
497 491 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
498 492 except error.RepoError as inst:
499 493 # RepoError can be raised while trying to read shared source
500 494 # configuration
501 495 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
502 496 if inst.hint:
503 497 self.ui.error(_(b"(%s)\n") % inst.hint)
504 498 errorraised = True
505 499 except error.Error as inst:
506 500 if inst.detailed_exit_code is not None:
507 501 detailed_exit_code = inst.detailed_exit_code
508 502 self.ui.error(inst.format())
509 503 errorraised = True
510 504
511 505 if errorraised:
512 506 self.ui.flush()
513 507 exit_code = 255
514 508 if self.ui.configbool(b'ui', b'detailed-exit-code'):
515 509 exit_code = detailed_exit_code
516 510 self.cresult.write(b'exit %d' % exit_code)
517 511 return
518 512 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
519 513 insts = []
520 514 if newhash.mtimehash != self.hashstate.mtimehash:
521 515 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
522 516 insts.append(b'unlink %s' % addr)
523 517 # mtimehash is empty if one or more extensions fail to load.
524 518 # to be compatible with hg, still serve the client this time.
525 519 if self.hashstate.mtimehash:
526 520 insts.append(b'reconnect')
527 521 if newhash.confighash != self.hashstate.confighash:
528 522 addr = _hashaddress(self.baseaddress, newhash.confighash)
529 523 insts.append(b'redirect %s' % addr)
530 524 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
531 525 self.cresult.write(b'\0'.join(insts) or b'\0')
532 526
533 527 def chdir(self):
534 528 """Change current directory
535 529
536 530 Note that the behavior of --cwd option is bit different from this.
537 531 It does not affect --config parameter.
538 532 """
539 533 path = self._readstr()
540 534 if not path:
541 535 return
542 536 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
543 537 os.chdir(path)
544 538
545 539 def setumask(self):
546 540 """Change umask (DEPRECATED)"""
547 541 # BUG: this does not follow the message frame structure, but kept for
548 542 # backward compatibility with old chg clients for some time
549 543 self._setumask(self._read(4))
550 544
551 545 def setumask2(self):
552 546 """Change umask"""
553 547 data = self._readstr()
554 548 if len(data) != 4:
555 549 raise ValueError(b'invalid mask length in setumask2 request')
556 550 self._setumask(data)
557 551
558 552 def _setumask(self, data):
559 553 mask = struct.unpack(b'>I', data)[0]
560 554 self.ui.log(b'chgserver', b'setumask %r\n', mask)
561 555 util.setumask(mask)
562 556
563 557 def runcommand(self):
564 558 # pager may be attached within the runcommand session, which should
565 559 # be detached at the end of the session. otherwise the pager wouldn't
566 560 # receive EOF.
567 561 globaloldios = self._oldios
568 562 self._oldios = []
569 563 try:
570 564 return super(chgcmdserver, self).runcommand()
571 565 finally:
572 566 self._restoreio()
573 567 self._oldios = globaloldios
574 568
575 569 def setenv(self):
576 570 """Clear and update os.environ
577 571
578 572 Note that not all variables can make an effect on the running process.
579 573 """
580 574 l = self._readlist()
581 575 try:
582 576 newenv = dict(s.split(b'=', 1) for s in l)
583 577 except ValueError:
584 578 raise ValueError(b'unexpected value in setenv request')
585 579 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
586 580
587 581 encoding.environ.clear()
588 582 encoding.environ.update(newenv)
589 583
590 584 capabilities = commandserver.server.capabilities.copy()
591 585 capabilities.update(
592 586 {
593 587 b'attachio': attachio,
594 588 b'chdir': chdir,
595 589 b'runcommand': runcommand,
596 590 b'setenv': setenv,
597 591 b'setumask': setumask,
598 592 b'setumask2': setumask2,
599 593 }
600 594 )
601 595
602 596 if util.safehasattr(procutil, b'setprocname'):
603 597
604 598 def setprocname(self):
605 599 """Change process title"""
606 600 name = self._readstr()
607 601 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
608 602 procutil.setprocname(name)
609 603
610 604 capabilities[b'setprocname'] = setprocname
611 605
612 606
613 607 def _tempaddress(address):
614 608 return b'%s.%d.tmp' % (address, os.getpid())
615 609
616 610
617 611 def _hashaddress(address, hashstr):
618 612 # if the basename of address contains '.', use only the left part. this
619 613 # makes it possible for the client to pass 'server.tmp$PID' and follow by
620 614 # an atomic rename to avoid locking when spawning new servers.
621 615 dirname, basename = os.path.split(address)
622 616 basename = basename.split(b'.', 1)[0]
623 617 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
624 618
625 619
626 620 class chgunixservicehandler:
627 621 """Set of operations for chg services"""
628 622
629 623 pollinterval = 1 # [sec]
630 624
631 625 def __init__(self, ui):
632 626 self.ui = ui
633 627
634 628 # TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level)
635 629 # when 3.5 support is dropped.
636 630 self._hashstate = None # type: hashstate
637 631 self._baseaddress = None # type: bytes
638 632 self._realaddress = None # type: bytes
639 633
640 634 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
641 635 self._lastactive = time.time()
642 636
643 637 def bindsocket(self, sock, address):
644 638 self._inithashstate(address)
645 639 self._checkextensions()
646 640 self._bind(sock)
647 641 self._createsymlink()
648 642 # no "listening at" message should be printed to simulate hg behavior
649 643
650 644 def _inithashstate(self, address):
651 645 self._baseaddress = address
652 646 if self.ui.configbool(b'chgserver', b'skiphash'):
653 647 self._hashstate = None
654 648 self._realaddress = address
655 649 return
656 650 self._hashstate = hashstate.fromui(self.ui)
657 651 self._realaddress = _hashaddress(address, self._hashstate.confighash)
658 652
659 653 def _checkextensions(self):
660 654 if not self._hashstate:
661 655 return
662 656 if extensions.notloaded():
663 657 # one or more extensions failed to load. mtimehash becomes
664 658 # meaningless because we do not know the paths of those extensions.
665 659 # set mtimehash to an illegal hash value to invalidate the server.
666 660 self._hashstate.mtimehash = b''
667 661
668 662 def _bind(self, sock):
669 663 # use a unique temp address so we can stat the file and do ownership
670 664 # check later
671 665 tempaddress = _tempaddress(self._realaddress)
672 666 util.bindunixsocket(sock, tempaddress)
673 667 self._socketstat = os.stat(tempaddress)
674 668 sock.listen(socket.SOMAXCONN)
675 669 # rename will replace the old socket file if exists atomically. the
676 670 # old server will detect ownership change and exit.
677 671 util.rename(tempaddress, self._realaddress)
678 672
679 673 def _createsymlink(self):
680 674 if self._baseaddress == self._realaddress:
681 675 return
682 676 tempaddress = _tempaddress(self._baseaddress)
683 677 os.symlink(os.path.basename(self._realaddress), tempaddress)
684 678 util.rename(tempaddress, self._baseaddress)
685 679
686 680 def _issocketowner(self):
687 681 try:
688 682 st = os.stat(self._realaddress)
689 683 return (
690 684 st.st_ino == self._socketstat.st_ino
691 685 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
692 686 )
693 687 except OSError:
694 688 return False
695 689
696 690 def unlinksocket(self, address):
697 691 if not self._issocketowner():
698 692 return
699 693 # it is possible to have a race condition here that we may
700 694 # remove another server's socket file. but that's okay
701 695 # since that server will detect and exit automatically and
702 696 # the client will start a new server on demand.
703 697 util.tryunlink(self._realaddress)
704 698
705 699 def shouldexit(self):
706 700 if not self._issocketowner():
707 701 self.ui.log(
708 702 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
709 703 )
710 704 return True
711 705 if time.time() - self._lastactive > self._idletimeout:
712 706 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
713 707 return True
714 708 return False
715 709
716 710 def newconnection(self):
717 711 self._lastactive = time.time()
718 712
719 713 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
720 714 return chgcmdserver(
721 715 self.ui,
722 716 repo,
723 717 fin,
724 718 fout,
725 719 conn,
726 720 prereposetups,
727 721 self._hashstate,
728 722 self._baseaddress,
729 723 )
730 724
731 725
732 726 def chgunixservice(ui, repo, opts):
733 727 # CHGINTERNALMARK is set by chg client. It is an indication of things are
734 728 # started by chg so other code can do things accordingly, like disabling
735 729 # demandimport or detecting chg client started by chg client. When executed
736 730 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
737 731 # environ cleaner.
738 732 if b'CHGINTERNALMARK' in encoding.environ:
739 733 del encoding.environ[b'CHGINTERNALMARK']
740 734 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
741 735 # it thinks the current value is "C". This breaks the hash computation and
742 736 # causes chg to restart loop.
743 737 if b'CHGORIG_LC_CTYPE' in encoding.environ:
744 738 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
745 739 del encoding.environ[b'CHGORIG_LC_CTYPE']
746 740 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
747 741 if b'LC_CTYPE' in encoding.environ:
748 742 del encoding.environ[b'LC_CTYPE']
749 743 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
750 744
751 745 if repo:
752 746 # one chgserver can serve multiple repos. drop repo information
753 747 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
754 748 h = chgunixservicehandler(ui)
755 749 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now