##// END OF EJS Templates
chgserver: remove Python 2 branch...
Gregory Szorc -
r49757:1ba11c8f default
parent child Browse files
Show More
@@ -1,767 +1,758 b''
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 pycompat.iteritems(encoding.environ)
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(object):
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(object):
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 pycompat.iteritems(environ))
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 elif pycompat.ispy3:
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 else:
419 # Python 2 uses the I/O streams provided by the C library, so
420 # make it line-buffered explicitly. Otherwise the default would
421 # be decided on first write(), where fout could be a pager.
422 if fp.isatty():
423 bufsize = 1 # line buffered
424 else:
425 bufsize = -1 # system default
426 newfp = os.fdopen(fp.fileno(), mode, bufsize)
427 418 if newfp is not fp:
428 419 setattr(ui, fn, newfp)
429 420 setattr(self, cn, newfp)
430 421
431 422 self._ioattached = True
432 423 self.cresult.write(struct.pack(b'>i', len(clientfds)))
433 424
434 425 def _saveio(self):
435 426 if self._oldios:
436 427 return
437 428 ui = self.ui
438 429 for cn, fn, _mode in _iochannels:
439 430 ch = getattr(self, cn)
440 431 fp = getattr(ui, fn)
441 432 fd = os.dup(fp.fileno())
442 433 self._oldios.append((ch, fp, fd))
443 434
444 435 def _restoreio(self):
445 436 if not self._oldios:
446 437 return
447 438 nullfd = os.open(os.devnull, os.O_WRONLY)
448 439 ui = self.ui
449 440 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
450 441 newfp = getattr(ui, fn)
451 442 # On Python 2, newfp and fp may be separate file objects associated
452 443 # with the same fd, so we must close newfp while it's associated
453 444 # with the client. Otherwise the new associated fd would be closed
454 445 # when newfp gets deleted. On Python 3, newfp is just a wrapper
455 446 # around fp even if newfp is not fp, so deleting newfp is safe.
456 447 if not (pycompat.ispy3 or newfp is fp):
457 448 newfp.close()
458 449 # restore original fd: fp is open again
459 450 try:
460 451 if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
461 452 # Discard buffered data which couldn't be flushed because
462 453 # of EPIPE. The data should belong to the current session
463 454 # and should never persist.
464 455 os.dup2(nullfd, fp.fileno())
465 456 fp.flush()
466 457 os.dup2(fd, fp.fileno())
467 458 except OSError as err:
468 459 # According to issue6330, running chg on heavy loaded systems
469 460 # can lead to EBUSY. [man dup2] indicates that, on Linux,
470 461 # EBUSY comes from a race condition between open() and dup2().
471 462 # However it's not clear why open() race occurred for
472 463 # newfd=stdin/out/err.
473 464 self.ui.log(
474 465 b'chgserver',
475 466 b'got %s while duplicating %s\n',
476 467 stringutil.forcebytestr(err),
477 468 fn,
478 469 )
479 470 os.close(fd)
480 471 setattr(self, cn, ch)
481 472 setattr(ui, fn, fp)
482 473 os.close(nullfd)
483 474 del self._oldios[:]
484 475
485 476 def validate(self):
486 477 """Reload the config and check if the server is up to date
487 478
488 479 Read a list of '\0' separated arguments.
489 480 Write a non-empty list of '\0' separated instruction strings or '\0'
490 481 if the list is empty.
491 482 An instruction string could be either:
492 483 - "unlink $path", the client should unlink the path to stop the
493 484 outdated server.
494 485 - "redirect $path", the client should attempt to connect to $path
495 486 first. If it does not work, start a new server. It implies
496 487 "reconnect".
497 488 - "exit $n", the client should exit directly with code n.
498 489 This may happen if we cannot parse the config.
499 490 - "reconnect", the client should close the connection and
500 491 reconnect.
501 492 If neither "reconnect" nor "redirect" is included in the instruction
502 493 list, the client can continue with this server after completing all
503 494 the instructions.
504 495 """
505 496 args = self._readlist()
506 497 errorraised = False
507 498 detailed_exit_code = 255
508 499 try:
509 500 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
510 501 except error.RepoError as inst:
511 502 # RepoError can be raised while trying to read shared source
512 503 # configuration
513 504 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
514 505 if inst.hint:
515 506 self.ui.error(_(b"(%s)\n") % inst.hint)
516 507 errorraised = True
517 508 except error.Error as inst:
518 509 if inst.detailed_exit_code is not None:
519 510 detailed_exit_code = inst.detailed_exit_code
520 511 self.ui.error(inst.format())
521 512 errorraised = True
522 513
523 514 if errorraised:
524 515 self.ui.flush()
525 516 exit_code = 255
526 517 if self.ui.configbool(b'ui', b'detailed-exit-code'):
527 518 exit_code = detailed_exit_code
528 519 self.cresult.write(b'exit %d' % exit_code)
529 520 return
530 521 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
531 522 insts = []
532 523 if newhash.mtimehash != self.hashstate.mtimehash:
533 524 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
534 525 insts.append(b'unlink %s' % addr)
535 526 # mtimehash is empty if one or more extensions fail to load.
536 527 # to be compatible with hg, still serve the client this time.
537 528 if self.hashstate.mtimehash:
538 529 insts.append(b'reconnect')
539 530 if newhash.confighash != self.hashstate.confighash:
540 531 addr = _hashaddress(self.baseaddress, newhash.confighash)
541 532 insts.append(b'redirect %s' % addr)
542 533 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
543 534 self.cresult.write(b'\0'.join(insts) or b'\0')
544 535
545 536 def chdir(self):
546 537 """Change current directory
547 538
548 539 Note that the behavior of --cwd option is bit different from this.
549 540 It does not affect --config parameter.
550 541 """
551 542 path = self._readstr()
552 543 if not path:
553 544 return
554 545 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
555 546 os.chdir(path)
556 547
557 548 def setumask(self):
558 549 """Change umask (DEPRECATED)"""
559 550 # BUG: this does not follow the message frame structure, but kept for
560 551 # backward compatibility with old chg clients for some time
561 552 self._setumask(self._read(4))
562 553
563 554 def setumask2(self):
564 555 """Change umask"""
565 556 data = self._readstr()
566 557 if len(data) != 4:
567 558 raise ValueError(b'invalid mask length in setumask2 request')
568 559 self._setumask(data)
569 560
570 561 def _setumask(self, data):
571 562 mask = struct.unpack(b'>I', data)[0]
572 563 self.ui.log(b'chgserver', b'setumask %r\n', mask)
573 564 util.setumask(mask)
574 565
575 566 def runcommand(self):
576 567 # pager may be attached within the runcommand session, which should
577 568 # be detached at the end of the session. otherwise the pager wouldn't
578 569 # receive EOF.
579 570 globaloldios = self._oldios
580 571 self._oldios = []
581 572 try:
582 573 return super(chgcmdserver, self).runcommand()
583 574 finally:
584 575 self._restoreio()
585 576 self._oldios = globaloldios
586 577
587 578 def setenv(self):
588 579 """Clear and update os.environ
589 580
590 581 Note that not all variables can make an effect on the running process.
591 582 """
592 583 l = self._readlist()
593 584 try:
594 585 newenv = dict(s.split(b'=', 1) for s in l)
595 586 except ValueError:
596 587 raise ValueError(b'unexpected value in setenv request')
597 588 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
598 589
599 590 encoding.environ.clear()
600 591 encoding.environ.update(newenv)
601 592
602 593 capabilities = commandserver.server.capabilities.copy()
603 594 capabilities.update(
604 595 {
605 596 b'attachio': attachio,
606 597 b'chdir': chdir,
607 598 b'runcommand': runcommand,
608 599 b'setenv': setenv,
609 600 b'setumask': setumask,
610 601 b'setumask2': setumask2,
611 602 }
612 603 )
613 604
614 605 if util.safehasattr(procutil, b'setprocname'):
615 606
616 607 def setprocname(self):
617 608 """Change process title"""
618 609 name = self._readstr()
619 610 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
620 611 procutil.setprocname(name)
621 612
622 613 capabilities[b'setprocname'] = setprocname
623 614
624 615
625 616 def _tempaddress(address):
626 617 return b'%s.%d.tmp' % (address, os.getpid())
627 618
628 619
629 620 def _hashaddress(address, hashstr):
630 621 # if the basename of address contains '.', use only the left part. this
631 622 # makes it possible for the client to pass 'server.tmp$PID' and follow by
632 623 # an atomic rename to avoid locking when spawning new servers.
633 624 dirname, basename = os.path.split(address)
634 625 basename = basename.split(b'.', 1)[0]
635 626 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
636 627
637 628
638 629 class chgunixservicehandler(object):
639 630 """Set of operations for chg services"""
640 631
641 632 pollinterval = 1 # [sec]
642 633
643 634 def __init__(self, ui):
644 635 self.ui = ui
645 636
646 637 # TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level)
647 638 # when 3.5 support is dropped.
648 639 self._hashstate = None # type: hashstate
649 640 self._baseaddress = None # type: bytes
650 641 self._realaddress = None # type: bytes
651 642
652 643 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
653 644 self._lastactive = time.time()
654 645
655 646 def bindsocket(self, sock, address):
656 647 self._inithashstate(address)
657 648 self._checkextensions()
658 649 self._bind(sock)
659 650 self._createsymlink()
660 651 # no "listening at" message should be printed to simulate hg behavior
661 652
662 653 def _inithashstate(self, address):
663 654 self._baseaddress = address
664 655 if self.ui.configbool(b'chgserver', b'skiphash'):
665 656 self._hashstate = None
666 657 self._realaddress = address
667 658 return
668 659 self._hashstate = hashstate.fromui(self.ui)
669 660 self._realaddress = _hashaddress(address, self._hashstate.confighash)
670 661
671 662 def _checkextensions(self):
672 663 if not self._hashstate:
673 664 return
674 665 if extensions.notloaded():
675 666 # one or more extensions failed to load. mtimehash becomes
676 667 # meaningless because we do not know the paths of those extensions.
677 668 # set mtimehash to an illegal hash value to invalidate the server.
678 669 self._hashstate.mtimehash = b''
679 670
680 671 def _bind(self, sock):
681 672 # use a unique temp address so we can stat the file and do ownership
682 673 # check later
683 674 tempaddress = _tempaddress(self._realaddress)
684 675 util.bindunixsocket(sock, tempaddress)
685 676 self._socketstat = os.stat(tempaddress)
686 677 sock.listen(socket.SOMAXCONN)
687 678 # rename will replace the old socket file if exists atomically. the
688 679 # old server will detect ownership change and exit.
689 680 util.rename(tempaddress, self._realaddress)
690 681
691 682 def _createsymlink(self):
692 683 if self._baseaddress == self._realaddress:
693 684 return
694 685 tempaddress = _tempaddress(self._baseaddress)
695 686 os.symlink(os.path.basename(self._realaddress), tempaddress)
696 687 util.rename(tempaddress, self._baseaddress)
697 688
698 689 def _issocketowner(self):
699 690 try:
700 691 st = os.stat(self._realaddress)
701 692 return (
702 693 st.st_ino == self._socketstat.st_ino
703 694 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
704 695 )
705 696 except OSError:
706 697 return False
707 698
708 699 def unlinksocket(self, address):
709 700 if not self._issocketowner():
710 701 return
711 702 # it is possible to have a race condition here that we may
712 703 # remove another server's socket file. but that's okay
713 704 # since that server will detect and exit automatically and
714 705 # the client will start a new server on demand.
715 706 util.tryunlink(self._realaddress)
716 707
717 708 def shouldexit(self):
718 709 if not self._issocketowner():
719 710 self.ui.log(
720 711 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
721 712 )
722 713 return True
723 714 if time.time() - self._lastactive > self._idletimeout:
724 715 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
725 716 return True
726 717 return False
727 718
728 719 def newconnection(self):
729 720 self._lastactive = time.time()
730 721
731 722 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
732 723 return chgcmdserver(
733 724 self.ui,
734 725 repo,
735 726 fin,
736 727 fout,
737 728 conn,
738 729 prereposetups,
739 730 self._hashstate,
740 731 self._baseaddress,
741 732 )
742 733
743 734
744 735 def chgunixservice(ui, repo, opts):
745 736 # CHGINTERNALMARK is set by chg client. It is an indication of things are
746 737 # started by chg so other code can do things accordingly, like disabling
747 738 # demandimport or detecting chg client started by chg client. When executed
748 739 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
749 740 # environ cleaner.
750 741 if b'CHGINTERNALMARK' in encoding.environ:
751 742 del encoding.environ[b'CHGINTERNALMARK']
752 743 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
753 744 # it thinks the current value is "C". This breaks the hash computation and
754 745 # causes chg to restart loop.
755 746 if b'CHGORIG_LC_CTYPE' in encoding.environ:
756 747 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
757 748 del encoding.environ[b'CHGORIG_LC_CTYPE']
758 749 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
759 750 if b'LC_CTYPE' in encoding.environ:
760 751 del encoding.environ[b'LC_CTYPE']
761 752 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
762 753
763 754 if repo:
764 755 # one chgserver can serve multiple repos. drop repo information
765 756 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
766 757 h = chgunixservicehandler(ui)
767 758 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now