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