##// END OF EJS Templates
py3: fix EOL detection in commandserver.channeledinput...
Yuya Nishihara -
r44857:98c14f01 stable
parent child Browse files
Show More
@@ -1,727 +1,727 b''
1 1 # commandserver.py - communicate with Mercurial's API over a pipe
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import gc
12 12 import os
13 13 import random
14 14 import signal
15 15 import socket
16 16 import struct
17 17 import traceback
18 18
19 19 try:
20 20 import selectors
21 21
22 22 selectors.BaseSelector
23 23 except ImportError:
24 24 from .thirdparty import selectors2 as selectors
25 25
26 26 from .i18n import _
27 27 from .pycompat import getattr
28 28 from . import (
29 29 encoding,
30 30 error,
31 31 loggingutil,
32 32 pycompat,
33 33 repocache,
34 34 util,
35 35 vfs as vfsmod,
36 36 )
37 37 from .utils import (
38 38 cborutil,
39 39 procutil,
40 40 )
41 41
42 42
43 43 class channeledoutput(object):
44 44 """
45 45 Write data to out in the following format:
46 46
47 47 data length (unsigned int),
48 48 data
49 49 """
50 50
51 51 def __init__(self, out, channel):
52 52 self.out = out
53 53 self.channel = channel
54 54
55 55 @property
56 56 def name(self):
57 57 return b'<%c-channel>' % self.channel
58 58
59 59 def write(self, data):
60 60 if not data:
61 61 return
62 62 # single write() to guarantee the same atomicity as the underlying file
63 63 self.out.write(struct.pack(b'>cI', self.channel, len(data)) + data)
64 64 self.out.flush()
65 65
66 66 def __getattr__(self, attr):
67 67 if attr in ('isatty', 'fileno', 'tell', 'seek'):
68 68 raise AttributeError(attr)
69 69 return getattr(self.out, attr)
70 70
71 71
72 72 class channeledmessage(object):
73 73 """
74 74 Write encoded message and metadata to out in the following format:
75 75
76 76 data length (unsigned int),
77 77 encoded message and metadata, as a flat key-value dict.
78 78
79 79 Each message should have 'type' attribute. Messages of unknown type
80 80 should be ignored.
81 81 """
82 82
83 83 # teach ui that write() can take **opts
84 84 structured = True
85 85
86 86 def __init__(self, out, channel, encodename, encodefn):
87 87 self._cout = channeledoutput(out, channel)
88 88 self.encoding = encodename
89 89 self._encodefn = encodefn
90 90
91 91 def write(self, data, **opts):
92 92 opts = pycompat.byteskwargs(opts)
93 93 if data is not None:
94 94 opts[b'data'] = data
95 95 self._cout.write(self._encodefn(opts))
96 96
97 97 def __getattr__(self, attr):
98 98 return getattr(self._cout, attr)
99 99
100 100
101 101 class channeledinput(object):
102 102 """
103 103 Read data from in_.
104 104
105 105 Requests for input are written to out in the following format:
106 106 channel identifier - 'I' for plain input, 'L' line based (1 byte)
107 107 how many bytes to send at most (unsigned int),
108 108
109 109 The client replies with:
110 110 data length (unsigned int), 0 meaning EOF
111 111 data
112 112 """
113 113
114 114 maxchunksize = 4 * 1024
115 115
116 116 def __init__(self, in_, out, channel):
117 117 self.in_ = in_
118 118 self.out = out
119 119 self.channel = channel
120 120
121 121 @property
122 122 def name(self):
123 123 return b'<%c-channel>' % self.channel
124 124
125 125 def read(self, size=-1):
126 126 if size < 0:
127 127 # if we need to consume all the clients input, ask for 4k chunks
128 128 # so the pipe doesn't fill up risking a deadlock
129 129 size = self.maxchunksize
130 130 s = self._read(size, self.channel)
131 131 buf = s
132 132 while s:
133 133 s = self._read(size, self.channel)
134 134 buf += s
135 135
136 136 return buf
137 137 else:
138 138 return self._read(size, self.channel)
139 139
140 140 def _read(self, size, channel):
141 141 if not size:
142 142 return b''
143 143 assert size > 0
144 144
145 145 # tell the client we need at most size bytes
146 146 self.out.write(struct.pack(b'>cI', channel, size))
147 147 self.out.flush()
148 148
149 149 length = self.in_.read(4)
150 150 length = struct.unpack(b'>I', length)[0]
151 151 if not length:
152 152 return b''
153 153 else:
154 154 return self.in_.read(length)
155 155
156 156 def readline(self, size=-1):
157 157 if size < 0:
158 158 size = self.maxchunksize
159 159 s = self._read(size, b'L')
160 160 buf = s
161 161 # keep asking for more until there's either no more or
162 162 # we got a full line
163 while s and s[-1] != b'\n':
163 while s and not s.endswith(b'\n'):
164 164 s = self._read(size, b'L')
165 165 buf += s
166 166
167 167 return buf
168 168 else:
169 169 return self._read(size, b'L')
170 170
171 171 def __iter__(self):
172 172 return self
173 173
174 174 def next(self):
175 175 l = self.readline()
176 176 if not l:
177 177 raise StopIteration
178 178 return l
179 179
180 180 __next__ = next
181 181
182 182 def __getattr__(self, attr):
183 183 if attr in ('isatty', 'fileno', 'tell', 'seek'):
184 184 raise AttributeError(attr)
185 185 return getattr(self.in_, attr)
186 186
187 187
188 188 _messageencoders = {
189 189 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
190 190 }
191 191
192 192
193 193 def _selectmessageencoder(ui):
194 194 # experimental config: cmdserver.message-encodings
195 195 encnames = ui.configlist(b'cmdserver', b'message-encodings')
196 196 for n in encnames:
197 197 f = _messageencoders.get(n)
198 198 if f:
199 199 return n, f
200 200 raise error.Abort(
201 201 b'no supported message encodings: %s' % b' '.join(encnames)
202 202 )
203 203
204 204
205 205 class server(object):
206 206 """
207 207 Listens for commands on fin, runs them and writes the output on a channel
208 208 based stream to fout.
209 209 """
210 210
211 211 def __init__(self, ui, repo, fin, fout, prereposetups=None):
212 212 self.cwd = encoding.getcwd()
213 213
214 214 if repo:
215 215 # the ui here is really the repo ui so take its baseui so we don't
216 216 # end up with its local configuration
217 217 self.ui = repo.baseui
218 218 self.repo = repo
219 219 self.repoui = repo.ui
220 220 else:
221 221 self.ui = ui
222 222 self.repo = self.repoui = None
223 223 self._prereposetups = prereposetups
224 224
225 225 self.cdebug = channeledoutput(fout, b'd')
226 226 self.cerr = channeledoutput(fout, b'e')
227 227 self.cout = channeledoutput(fout, b'o')
228 228 self.cin = channeledinput(fin, fout, b'I')
229 229 self.cresult = channeledoutput(fout, b'r')
230 230
231 231 if self.ui.config(b'cmdserver', b'log') == b'-':
232 232 # switch log stream of server's ui to the 'd' (debug) channel
233 233 # (don't touch repo.ui as its lifetime is longer than the server)
234 234 self.ui = self.ui.copy()
235 235 setuplogging(self.ui, repo=None, fp=self.cdebug)
236 236
237 237 # TODO: add this to help/config.txt when stabilized
238 238 # ``channel``
239 239 # Use separate channel for structured output. (Command-server only)
240 240 self.cmsg = None
241 241 if ui.config(b'ui', b'message-output') == b'channel':
242 242 encname, encfn = _selectmessageencoder(ui)
243 243 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
244 244
245 245 self.client = fin
246 246
247 247 def cleanup(self):
248 248 """release and restore resources taken during server session"""
249 249
250 250 def _read(self, size):
251 251 if not size:
252 252 return b''
253 253
254 254 data = self.client.read(size)
255 255
256 256 # is the other end closed?
257 257 if not data:
258 258 raise EOFError
259 259
260 260 return data
261 261
262 262 def _readstr(self):
263 263 """read a string from the channel
264 264
265 265 format:
266 266 data length (uint32), data
267 267 """
268 268 length = struct.unpack(b'>I', self._read(4))[0]
269 269 if not length:
270 270 return b''
271 271 return self._read(length)
272 272
273 273 def _readlist(self):
274 274 """read a list of NULL separated strings from the channel"""
275 275 s = self._readstr()
276 276 if s:
277 277 return s.split(b'\0')
278 278 else:
279 279 return []
280 280
281 281 def runcommand(self):
282 282 """ reads a list of \0 terminated arguments, executes
283 283 and writes the return code to the result channel """
284 284 from . import dispatch # avoid cycle
285 285
286 286 args = self._readlist()
287 287
288 288 # copy the uis so changes (e.g. --config or --verbose) don't
289 289 # persist between requests
290 290 copiedui = self.ui.copy()
291 291 uis = [copiedui]
292 292 if self.repo:
293 293 self.repo.baseui = copiedui
294 294 # clone ui without using ui.copy because this is protected
295 295 repoui = self.repoui.__class__(self.repoui)
296 296 repoui.copy = copiedui.copy # redo copy protection
297 297 uis.append(repoui)
298 298 self.repo.ui = self.repo.dirstate._ui = repoui
299 299 self.repo.invalidateall()
300 300
301 301 for ui in uis:
302 302 ui.resetstate()
303 303 # any kind of interaction must use server channels, but chg may
304 304 # replace channels by fully functional tty files. so nontty is
305 305 # enforced only if cin is a channel.
306 306 if not util.safehasattr(self.cin, b'fileno'):
307 307 ui.setconfig(b'ui', b'nontty', b'true', b'commandserver')
308 308
309 309 req = dispatch.request(
310 310 args[:],
311 311 copiedui,
312 312 self.repo,
313 313 self.cin,
314 314 self.cout,
315 315 self.cerr,
316 316 self.cmsg,
317 317 prereposetups=self._prereposetups,
318 318 )
319 319
320 320 try:
321 321 ret = dispatch.dispatch(req) & 255
322 322 self.cresult.write(struct.pack(b'>i', int(ret)))
323 323 finally:
324 324 # restore old cwd
325 325 if b'--cwd' in args:
326 326 os.chdir(self.cwd)
327 327
328 328 def getencoding(self):
329 329 """ writes the current encoding to the result channel """
330 330 self.cresult.write(encoding.encoding)
331 331
332 332 def serveone(self):
333 333 cmd = self.client.readline()[:-1]
334 334 if cmd:
335 335 handler = self.capabilities.get(cmd)
336 336 if handler:
337 337 handler(self)
338 338 else:
339 339 # clients are expected to check what commands are supported by
340 340 # looking at the servers capabilities
341 341 raise error.Abort(_(b'unknown command %s') % cmd)
342 342
343 343 return cmd != b''
344 344
345 345 capabilities = {b'runcommand': runcommand, b'getencoding': getencoding}
346 346
347 347 def serve(self):
348 348 hellomsg = b'capabilities: ' + b' '.join(sorted(self.capabilities))
349 349 hellomsg += b'\n'
350 350 hellomsg += b'encoding: ' + encoding.encoding
351 351 hellomsg += b'\n'
352 352 if self.cmsg:
353 353 hellomsg += b'message-encoding: %s\n' % self.cmsg.encoding
354 354 hellomsg += b'pid: %d' % procutil.getpid()
355 355 if util.safehasattr(os, b'getpgid'):
356 356 hellomsg += b'\n'
357 357 hellomsg += b'pgid: %d' % os.getpgid(0)
358 358
359 359 # write the hello msg in -one- chunk
360 360 self.cout.write(hellomsg)
361 361
362 362 try:
363 363 while self.serveone():
364 364 pass
365 365 except EOFError:
366 366 # we'll get here if the client disconnected while we were reading
367 367 # its request
368 368 return 1
369 369
370 370 return 0
371 371
372 372
373 373 def setuplogging(ui, repo=None, fp=None):
374 374 """Set up server logging facility
375 375
376 376 If cmdserver.log is '-', log messages will be sent to the given fp.
377 377 It should be the 'd' channel while a client is connected, and otherwise
378 378 is the stderr of the server process.
379 379 """
380 380 # developer config: cmdserver.log
381 381 logpath = ui.config(b'cmdserver', b'log')
382 382 if not logpath:
383 383 return
384 384 # developer config: cmdserver.track-log
385 385 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
386 386
387 387 if logpath == b'-' and fp:
388 388 logger = loggingutil.fileobjectlogger(fp, tracked)
389 389 elif logpath == b'-':
390 390 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
391 391 else:
392 392 logpath = os.path.abspath(util.expandpath(logpath))
393 393 # developer config: cmdserver.max-log-files
394 394 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
395 395 # developer config: cmdserver.max-log-size
396 396 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
397 397 vfs = vfsmod.vfs(os.path.dirname(logpath))
398 398 logger = loggingutil.filelogger(
399 399 vfs,
400 400 os.path.basename(logpath),
401 401 tracked,
402 402 maxfiles=maxfiles,
403 403 maxsize=maxsize,
404 404 )
405 405
406 406 targetuis = {ui}
407 407 if repo:
408 408 targetuis.add(repo.baseui)
409 409 targetuis.add(repo.ui)
410 410 for u in targetuis:
411 411 u.setlogger(b'cmdserver', logger)
412 412
413 413
414 414 class pipeservice(object):
415 415 def __init__(self, ui, repo, opts):
416 416 self.ui = ui
417 417 self.repo = repo
418 418
419 419 def init(self):
420 420 pass
421 421
422 422 def run(self):
423 423 ui = self.ui
424 424 # redirect stdio to null device so that broken extensions or in-process
425 425 # hooks will never cause corruption of channel protocol.
426 426 with ui.protectedfinout() as (fin, fout):
427 427 sv = server(ui, self.repo, fin, fout)
428 428 try:
429 429 return sv.serve()
430 430 finally:
431 431 sv.cleanup()
432 432
433 433
434 434 def _initworkerprocess():
435 435 # use a different process group from the master process, in order to:
436 436 # 1. make the current process group no longer "orphaned" (because the
437 437 # parent of this process is in a different process group while
438 438 # remains in a same session)
439 439 # according to POSIX 2.2.2.52, orphaned process group will ignore
440 440 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
441 441 # cause trouble for things like ncurses.
442 442 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
443 443 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
444 444 # processes like ssh will be killed properly, without affecting
445 445 # unrelated processes.
446 446 os.setpgid(0, 0)
447 447 # change random state otherwise forked request handlers would have a
448 448 # same state inherited from parent.
449 449 random.seed()
450 450
451 451
452 452 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
453 453 fin = conn.makefile('rb')
454 454 fout = conn.makefile('wb')
455 455 sv = None
456 456 try:
457 457 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
458 458 try:
459 459 sv.serve()
460 460 # handle exceptions that may be raised by command server. most of
461 461 # known exceptions are caught by dispatch.
462 462 except error.Abort as inst:
463 463 ui.error(_(b'abort: %s\n') % inst)
464 464 except IOError as inst:
465 465 if inst.errno != errno.EPIPE:
466 466 raise
467 467 except KeyboardInterrupt:
468 468 pass
469 469 finally:
470 470 sv.cleanup()
471 471 except: # re-raises
472 472 # also write traceback to error channel. otherwise client cannot
473 473 # see it because it is written to server's stderr by default.
474 474 if sv:
475 475 cerr = sv.cerr
476 476 else:
477 477 cerr = channeledoutput(fout, b'e')
478 478 cerr.write(encoding.strtolocal(traceback.format_exc()))
479 479 raise
480 480 finally:
481 481 fin.close()
482 482 try:
483 483 fout.close() # implicit flush() may cause another EPIPE
484 484 except IOError as inst:
485 485 if inst.errno != errno.EPIPE:
486 486 raise
487 487
488 488
489 489 class unixservicehandler(object):
490 490 """Set of pluggable operations for unix-mode services
491 491
492 492 Almost all methods except for createcmdserver() are called in the main
493 493 process. You can't pass mutable resource back from createcmdserver().
494 494 """
495 495
496 496 pollinterval = None
497 497
498 498 def __init__(self, ui):
499 499 self.ui = ui
500 500
501 501 def bindsocket(self, sock, address):
502 502 util.bindunixsocket(sock, address)
503 503 sock.listen(socket.SOMAXCONN)
504 504 self.ui.status(_(b'listening at %s\n') % address)
505 505 self.ui.flush() # avoid buffering of status message
506 506
507 507 def unlinksocket(self, address):
508 508 os.unlink(address)
509 509
510 510 def shouldexit(self):
511 511 """True if server should shut down; checked per pollinterval"""
512 512 return False
513 513
514 514 def newconnection(self):
515 515 """Called when main process notices new connection"""
516 516
517 517 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
518 518 """Create new command server instance; called in the process that
519 519 serves for the current connection"""
520 520 return server(self.ui, repo, fin, fout, prereposetups)
521 521
522 522
523 523 class unixforkingservice(object):
524 524 """
525 525 Listens on unix domain socket and forks server per connection
526 526 """
527 527
528 528 def __init__(self, ui, repo, opts, handler=None):
529 529 self.ui = ui
530 530 self.repo = repo
531 531 self.address = opts[b'address']
532 532 if not util.safehasattr(socket, b'AF_UNIX'):
533 533 raise error.Abort(_(b'unsupported platform'))
534 534 if not self.address:
535 535 raise error.Abort(_(b'no socket path specified with --address'))
536 536 self._servicehandler = handler or unixservicehandler(ui)
537 537 self._sock = None
538 538 self._mainipc = None
539 539 self._workeripc = None
540 540 self._oldsigchldhandler = None
541 541 self._workerpids = set() # updated by signal handler; do not iterate
542 542 self._socketunlinked = None
543 543 # experimental config: cmdserver.max-repo-cache
544 544 maxlen = ui.configint(b'cmdserver', b'max-repo-cache')
545 545 if maxlen < 0:
546 546 raise error.Abort(_(b'negative max-repo-cache size not allowed'))
547 547 self._repoloader = repocache.repoloader(ui, maxlen)
548 548
549 549 def init(self):
550 550 self._sock = socket.socket(socket.AF_UNIX)
551 551 # IPC channel from many workers to one main process; this is actually
552 552 # a uni-directional pipe, but is backed by a DGRAM socket so each
553 553 # message can be easily separated.
554 554 o = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
555 555 self._mainipc, self._workeripc = o
556 556 self._servicehandler.bindsocket(self._sock, self.address)
557 557 if util.safehasattr(procutil, b'unblocksignal'):
558 558 procutil.unblocksignal(signal.SIGCHLD)
559 559 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
560 560 self._oldsigchldhandler = o
561 561 self._socketunlinked = False
562 562 self._repoloader.start()
563 563
564 564 def _unlinksocket(self):
565 565 if not self._socketunlinked:
566 566 self._servicehandler.unlinksocket(self.address)
567 567 self._socketunlinked = True
568 568
569 569 def _cleanup(self):
570 570 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
571 571 self._sock.close()
572 572 self._mainipc.close()
573 573 self._workeripc.close()
574 574 self._unlinksocket()
575 575 self._repoloader.stop()
576 576 # don't kill child processes as they have active clients, just wait
577 577 self._reapworkers(0)
578 578
579 579 def run(self):
580 580 try:
581 581 self._mainloop()
582 582 finally:
583 583 self._cleanup()
584 584
585 585 def _mainloop(self):
586 586 exiting = False
587 587 h = self._servicehandler
588 588 selector = selectors.DefaultSelector()
589 589 selector.register(
590 590 self._sock, selectors.EVENT_READ, self._acceptnewconnection
591 591 )
592 592 selector.register(
593 593 self._mainipc, selectors.EVENT_READ, self._handlemainipc
594 594 )
595 595 while True:
596 596 if not exiting and h.shouldexit():
597 597 # clients can no longer connect() to the domain socket, so
598 598 # we stop queuing new requests.
599 599 # for requests that are queued (connect()-ed, but haven't been
600 600 # accept()-ed), handle them before exit. otherwise, clients
601 601 # waiting for recv() will receive ECONNRESET.
602 602 self._unlinksocket()
603 603 exiting = True
604 604 try:
605 605 events = selector.select(timeout=h.pollinterval)
606 606 except OSError as inst:
607 607 # selectors2 raises ETIMEDOUT if timeout exceeded while
608 608 # handling signal interrupt. That's probably wrong, but
609 609 # we can easily get around it.
610 610 if inst.errno != errno.ETIMEDOUT:
611 611 raise
612 612 events = []
613 613 if not events:
614 614 # only exit if we completed all queued requests
615 615 if exiting:
616 616 break
617 617 continue
618 618 for key, _mask in events:
619 619 key.data(key.fileobj, selector)
620 620 selector.close()
621 621
622 622 def _acceptnewconnection(self, sock, selector):
623 623 h = self._servicehandler
624 624 try:
625 625 conn, _addr = sock.accept()
626 626 except socket.error as inst:
627 627 if inst.args[0] == errno.EINTR:
628 628 return
629 629 raise
630 630
631 631 # Future improvement: On Python 3.7, maybe gc.freeze() can be used
632 632 # to prevent COW memory from being touched by GC.
633 633 # https://instagram-engineering.com/
634 634 # copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf
635 635 pid = os.fork()
636 636 if pid:
637 637 try:
638 638 self.ui.log(
639 639 b'cmdserver', b'forked worker process (pid=%d)\n', pid
640 640 )
641 641 self._workerpids.add(pid)
642 642 h.newconnection()
643 643 finally:
644 644 conn.close() # release handle in parent process
645 645 else:
646 646 try:
647 647 selector.close()
648 648 sock.close()
649 649 self._mainipc.close()
650 650 self._runworker(conn)
651 651 conn.close()
652 652 self._workeripc.close()
653 653 os._exit(0)
654 654 except: # never return, hence no re-raises
655 655 try:
656 656 self.ui.traceback(force=True)
657 657 finally:
658 658 os._exit(255)
659 659
660 660 def _handlemainipc(self, sock, selector):
661 661 """Process messages sent from a worker"""
662 662 try:
663 663 path = sock.recv(32768) # large enough to receive path
664 664 except socket.error as inst:
665 665 if inst.args[0] == errno.EINTR:
666 666 return
667 667 raise
668 668 self._repoloader.load(path)
669 669
670 670 def _sigchldhandler(self, signal, frame):
671 671 self._reapworkers(os.WNOHANG)
672 672
673 673 def _reapworkers(self, options):
674 674 while self._workerpids:
675 675 try:
676 676 pid, _status = os.waitpid(-1, options)
677 677 except OSError as inst:
678 678 if inst.errno == errno.EINTR:
679 679 continue
680 680 if inst.errno != errno.ECHILD:
681 681 raise
682 682 # no child processes at all (reaped by other waitpid()?)
683 683 self._workerpids.clear()
684 684 return
685 685 if pid == 0:
686 686 # no waitable child processes
687 687 return
688 688 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
689 689 self._workerpids.discard(pid)
690 690
691 691 def _runworker(self, conn):
692 692 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
693 693 _initworkerprocess()
694 694 h = self._servicehandler
695 695 try:
696 696 _serverequest(
697 697 self.ui,
698 698 self.repo,
699 699 conn,
700 700 h.createcmdserver,
701 701 prereposetups=[self._reposetup],
702 702 )
703 703 finally:
704 704 gc.collect() # trigger __del__ since worker process uses os._exit
705 705
706 706 def _reposetup(self, ui, repo):
707 707 if not repo.local():
708 708 return
709 709
710 710 class unixcmdserverrepo(repo.__class__):
711 711 def close(self):
712 712 super(unixcmdserverrepo, self).close()
713 713 try:
714 714 self._cmdserveripc.send(self.root)
715 715 except socket.error:
716 716 self.ui.log(
717 717 b'cmdserver', b'failed to send repo root to master\n'
718 718 )
719 719
720 720 repo.__class__ = unixcmdserverrepo
721 721 repo._cmdserveripc = self._workeripc
722 722
723 723 cachedrepo = self._repoloader.get(repo.root)
724 724 if cachedrepo is None:
725 725 return
726 726 repo.ui.log(b'repocache', b'repo from cache: %s\n', repo.root)
727 727 repocache.copycache(cachedrepo, repo)
@@ -1,1106 +1,1111 b''
1 1 #if windows
2 2 $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH"
3 3 #else
4 4 $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH"
5 5 #endif
6 6 $ export PYTHONPATH
7 7
8 8 typical client does not want echo-back messages, so test without it:
9 9
10 10 $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
11 11 $ mv $HGRCPATH.new $HGRCPATH
12 12
13 13 $ hg init repo
14 14 $ cd repo
15 15
16 16 >>> from __future__ import absolute_import
17 17 >>> import os
18 18 >>> import sys
19 19 >>> from hgclient import bprint, check, readchannel, runcommand
20 20 >>> @check
21 21 ... def hellomessage(server):
22 22 ... ch, data = readchannel(server)
23 23 ... bprint(b'%c, %r' % (ch, data))
24 24 ... # run an arbitrary command to make sure the next thing the server
25 25 ... # sends isn't part of the hello message
26 26 ... runcommand(server, [b'id'])
27 27 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
28 28 *** runcommand id
29 29 000000000000 tip
30 30
31 31 >>> from hgclient import check
32 32 >>> @check
33 33 ... def unknowncommand(server):
34 34 ... server.stdin.write(b'unknowncommand\n')
35 35 abort: unknown command unknowncommand
36 36
37 37 >>> from hgclient import check, readchannel, runcommand
38 38 >>> @check
39 39 ... def checkruncommand(server):
40 40 ... # hello block
41 41 ... readchannel(server)
42 42 ...
43 43 ... # no args
44 44 ... runcommand(server, [])
45 45 ...
46 46 ... # global options
47 47 ... runcommand(server, [b'id', b'--quiet'])
48 48 ...
49 49 ... # make sure global options don't stick through requests
50 50 ... runcommand(server, [b'id'])
51 51 ...
52 52 ... # --config
53 53 ... runcommand(server, [b'id', b'--config', b'ui.quiet=True'])
54 54 ...
55 55 ... # make sure --config doesn't stick
56 56 ... runcommand(server, [b'id'])
57 57 ...
58 58 ... # negative return code should be masked
59 59 ... runcommand(server, [b'id', b'-runknown'])
60 60 *** runcommand
61 61 Mercurial Distributed SCM
62 62
63 63 basic commands:
64 64
65 65 add add the specified files on the next commit
66 66 annotate show changeset information by line for each file
67 67 clone make a copy of an existing repository
68 68 commit commit the specified files or all outstanding changes
69 69 diff diff repository (or selected files)
70 70 export dump the header and diffs for one or more changesets
71 71 forget forget the specified files on the next commit
72 72 init create a new repository in the given directory
73 73 log show revision history of entire repository or files
74 74 merge merge another revision into working directory
75 75 pull pull changes from the specified source
76 76 push push changes to the specified destination
77 77 remove remove the specified files on the next commit
78 78 serve start stand-alone webserver
79 79 status show changed files in the working directory
80 80 summary summarize working directory state
81 81 update update working directory (or switch revisions)
82 82
83 83 (use 'hg help' for the full list of commands or 'hg -v' for details)
84 84 *** runcommand id --quiet
85 85 000000000000
86 86 *** runcommand id
87 87 000000000000 tip
88 88 *** runcommand id --config ui.quiet=True
89 89 000000000000
90 90 *** runcommand id
91 91 000000000000 tip
92 92 *** runcommand id -runknown
93 93 abort: unknown revision 'unknown'!
94 94 [255]
95 95
96 96 >>> from hgclient import bprint, check, readchannel
97 97 >>> @check
98 98 ... def inputeof(server):
99 99 ... readchannel(server)
100 100 ... server.stdin.write(b'runcommand\n')
101 101 ... # close stdin while server is waiting for input
102 102 ... server.stdin.close()
103 103 ...
104 104 ... # server exits with 1 if the pipe closed while reading the command
105 105 ... bprint(b'server exit code =', b'%d' % server.wait())
106 106 server exit code = 1
107 107
108 108 >>> from hgclient import check, readchannel, runcommand, stringio
109 109 >>> @check
110 110 ... def serverinput(server):
111 111 ... readchannel(server)
112 112 ...
113 113 ... patch = b"""
114 114 ... # HG changeset patch
115 115 ... # User test
116 116 ... # Date 0 0
117 117 ... # Node ID c103a3dec114d882c98382d684d8af798d09d857
118 118 ... # Parent 0000000000000000000000000000000000000000
119 119 ... 1
120 120 ...
121 121 ... diff -r 000000000000 -r c103a3dec114 a
122 122 ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000
123 123 ... +++ b/a Thu Jan 01 00:00:00 1970 +0000
124 124 ... @@ -0,0 +1,1 @@
125 125 ... +1
126 126 ... """
127 127 ...
128 128 ... runcommand(server, [b'import', b'-'], input=stringio(patch))
129 129 ... runcommand(server, [b'log'])
130 130 *** runcommand import -
131 131 applying patch from stdin
132 132 *** runcommand log
133 133 changeset: 0:eff892de26ec
134 134 tag: tip
135 135 user: test
136 136 date: Thu Jan 01 00:00:00 1970 +0000
137 137 summary: 1
138 138
139 139
140 140 check strict parsing of early options:
141 141
142 142 >>> import os
143 143 >>> from hgclient import check, readchannel, runcommand
144 144 >>> os.environ['HGPLAIN'] = '+strictflags'
145 145 >>> @check
146 146 ... def cwd(server):
147 147 ... readchannel(server)
148 148 ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned',
149 149 ... b'default'])
150 150 *** runcommand log -b --config=alias.log=!echo pwned default
151 151 abort: unknown revision '--config=alias.log=!echo pwned'!
152 152 [255]
153 153
154 154 check that "histedit --commands=-" can read rules from the input channel:
155 155
156 156 >>> from hgclient import check, readchannel, runcommand, stringio
157 157 >>> @check
158 158 ... def serverinput(server):
159 159 ... readchannel(server)
160 160 ... rules = b'pick eff892de26ec\n'
161 161 ... runcommand(server, [b'histedit', b'0', b'--commands=-',
162 162 ... b'--config', b'extensions.histedit='],
163 163 ... input=stringio(rules))
164 164 *** runcommand histedit 0 --commands=- --config extensions.histedit=
165 165
166 166 check that --cwd doesn't persist between requests:
167 167
168 168 $ mkdir foo
169 169 $ touch foo/bar
170 170 >>> from hgclient import check, readchannel, runcommand
171 171 >>> @check
172 172 ... def cwd(server):
173 173 ... readchannel(server)
174 174 ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar'])
175 175 ... runcommand(server, [b'st', b'foo/bar'])
176 176 *** runcommand --cwd foo st bar
177 177 ? bar
178 178 *** runcommand st foo/bar
179 179 ? foo/bar
180 180
181 181 $ rm foo/bar
182 182
183 183
184 184 check that local configs for the cached repo aren't inherited when -R is used:
185 185
186 186 $ cat <<EOF >> .hg/hgrc
187 187 > [ui]
188 188 > foo = bar
189 189 > EOF
190 190
191 191 #if no-extraextensions
192 192
193 193 >>> from hgclient import check, readchannel, runcommand, sep
194 194 >>> @check
195 195 ... def localhgrc(server):
196 196 ... readchannel(server)
197 197 ...
198 198 ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should
199 199 ... # show it
200 200 ... runcommand(server, [b'showconfig'], outfilter=sep)
201 201 ...
202 202 ... # but not for this repo
203 203 ... runcommand(server, [b'init', b'foo'])
204 204 ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults'])
205 205 *** runcommand showconfig
206 206 bundle.mainreporoot=$TESTTMP/repo
207 207 devel.all-warnings=true
208 208 devel.default-date=0 0
209 209 extensions.fsmonitor= (fsmonitor !)
210 210 largefiles.usercache=$TESTTMP/.cache/largefiles
211 211 lfs.usercache=$TESTTMP/.cache/lfs
212 212 ui.slash=True
213 213 ui.interactive=False
214 214 ui.merge=internal:merge
215 215 ui.mergemarkers=detailed
216 216 ui.foo=bar
217 217 ui.nontty=true
218 218 web.address=localhost
219 219 web\.ipv6=(?:True|False) (re)
220 220 web.server-header=testing stub value
221 221 *** runcommand init foo
222 222 *** runcommand -R foo showconfig ui defaults
223 223 ui.slash=True
224 224 ui.interactive=False
225 225 ui.merge=internal:merge
226 226 ui.mergemarkers=detailed
227 227 ui.nontty=true
228 228 #endif
229 229
230 230 $ rm -R foo
231 231
232 232 #if windows
233 233 $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH"
234 234 #else
235 235 $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH"
236 236 #endif
237 237
238 238 $ cat <<EOF > hook.py
239 239 > import sys
240 240 > from hgclient import bprint
241 241 > def hook(**args):
242 242 > bprint(b'hook talking')
243 243 > bprint(b'now try to read something: %r' % sys.stdin.read())
244 244 > EOF
245 245
246 246 >>> from hgclient import check, readchannel, runcommand, stringio
247 247 >>> @check
248 248 ... def hookoutput(server):
249 249 ... readchannel(server)
250 250 ... runcommand(server, [b'--config',
251 251 ... b'hooks.pre-identify=python:hook.hook',
252 252 ... b'id'],
253 253 ... input=stringio(b'some input'))
254 254 *** runcommand --config hooks.pre-identify=python:hook.hook id
255 255 eff892de26ec tip
256 256 hook talking
257 257 now try to read something: ''
258 258
259 259 Clean hook cached version
260 260 $ rm hook.py*
261 261 $ rm -Rf __pycache__
262 262
263 263 $ echo a >> a
264 264 >>> import os
265 265 >>> from hgclient import check, readchannel, runcommand
266 266 >>> @check
267 267 ... def outsidechanges(server):
268 268 ... readchannel(server)
269 269 ... runcommand(server, [b'status'])
270 270 ... os.system('hg ci -Am2')
271 271 ... runcommand(server, [b'tip'])
272 272 ... runcommand(server, [b'status'])
273 273 *** runcommand status
274 274 M a
275 275 *** runcommand tip
276 276 changeset: 1:d3a0a68be6de
277 277 tag: tip
278 278 user: test
279 279 date: Thu Jan 01 00:00:00 1970 +0000
280 280 summary: 2
281 281
282 282 *** runcommand status
283 283
284 284 >>> import os
285 285 >>> from hgclient import bprint, check, readchannel, runcommand
286 286 >>> @check
287 287 ... def bookmarks(server):
288 288 ... readchannel(server)
289 289 ... runcommand(server, [b'bookmarks'])
290 290 ...
291 291 ... # changes .hg/bookmarks
292 292 ... os.system('hg bookmark -i bm1')
293 293 ... os.system('hg bookmark -i bm2')
294 294 ... runcommand(server, [b'bookmarks'])
295 295 ...
296 296 ... # changes .hg/bookmarks.current
297 297 ... os.system('hg upd bm1 -q')
298 298 ... runcommand(server, [b'bookmarks'])
299 299 ...
300 300 ... runcommand(server, [b'bookmarks', b'bm3'])
301 301 ... f = open('a', 'ab')
302 302 ... f.write(b'a\n') and None
303 303 ... f.close()
304 304 ... runcommand(server, [b'commit', b'-Amm'])
305 305 ... runcommand(server, [b'bookmarks'])
306 306 ... bprint(b'')
307 307 *** runcommand bookmarks
308 308 no bookmarks set
309 309 *** runcommand bookmarks
310 310 bm1 1:d3a0a68be6de
311 311 bm2 1:d3a0a68be6de
312 312 *** runcommand bookmarks
313 313 * bm1 1:d3a0a68be6de
314 314 bm2 1:d3a0a68be6de
315 315 *** runcommand bookmarks bm3
316 316 *** runcommand commit -Amm
317 317 *** runcommand bookmarks
318 318 bm1 1:d3a0a68be6de
319 319 bm2 1:d3a0a68be6de
320 320 * bm3 2:aef17e88f5f0
321 321
322 322
323 323 >>> import os
324 324 >>> from hgclient import check, readchannel, runcommand
325 325 >>> @check
326 326 ... def tagscache(server):
327 327 ... readchannel(server)
328 328 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
329 329 ... os.system('hg tag -r 0 foo')
330 330 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
331 331 *** runcommand id -t -r 0
332 332
333 333 *** runcommand id -t -r 0
334 334 foo
335 335
336 336 >>> import os
337 337 >>> from hgclient import check, readchannel, runcommand
338 338 >>> @check
339 339 ... def setphase(server):
340 340 ... readchannel(server)
341 341 ... runcommand(server, [b'phase', b'-r', b'.'])
342 342 ... os.system('hg phase -r . -p')
343 343 ... runcommand(server, [b'phase', b'-r', b'.'])
344 344 *** runcommand phase -r .
345 345 3: draft
346 346 *** runcommand phase -r .
347 347 3: public
348 348
349 349 $ echo a >> a
350 350 >>> from hgclient import bprint, check, readchannel, runcommand
351 351 >>> @check
352 352 ... def rollback(server):
353 353 ... readchannel(server)
354 354 ... runcommand(server, [b'phase', b'-r', b'.', b'-p'])
355 355 ... runcommand(server, [b'commit', b'-Am.'])
356 356 ... runcommand(server, [b'rollback'])
357 357 ... runcommand(server, [b'phase', b'-r', b'.'])
358 358 ... bprint(b'')
359 359 *** runcommand phase -r . -p
360 360 no phases changed
361 361 *** runcommand commit -Am.
362 362 *** runcommand rollback
363 363 repository tip rolled back to revision 3 (undo commit)
364 364 working directory now based on revision 3
365 365 *** runcommand phase -r .
366 366 3: public
367 367
368 368
369 369 >>> import os
370 370 >>> from hgclient import check, readchannel, runcommand
371 371 >>> @check
372 372 ... def branch(server):
373 373 ... readchannel(server)
374 374 ... runcommand(server, [b'branch'])
375 375 ... os.system('hg branch foo')
376 376 ... runcommand(server, [b'branch'])
377 377 ... os.system('hg branch default')
378 378 *** runcommand branch
379 379 default
380 380 marked working directory as branch foo
381 381 (branches are permanent and global, did you want a bookmark?)
382 382 *** runcommand branch
383 383 foo
384 384 marked working directory as branch default
385 385 (branches are permanent and global, did you want a bookmark?)
386 386
387 387 $ touch .hgignore
388 388 >>> import os
389 389 >>> from hgclient import bprint, check, readchannel, runcommand
390 390 >>> @check
391 391 ... def hgignore(server):
392 392 ... readchannel(server)
393 393 ... runcommand(server, [b'commit', b'-Am.'])
394 394 ... f = open('ignored-file', 'ab')
395 395 ... f.write(b'') and None
396 396 ... f.close()
397 397 ... f = open('.hgignore', 'ab')
398 398 ... f.write(b'ignored-file')
399 399 ... f.close()
400 400 ... runcommand(server, [b'status', b'-i', b'-u'])
401 401 ... bprint(b'')
402 402 *** runcommand commit -Am.
403 403 adding .hgignore
404 404 *** runcommand status -i -u
405 405 I ignored-file
406 406
407 407
408 408 cache of non-public revisions should be invalidated on repository change
409 409 (issue4855):
410 410
411 411 >>> import os
412 412 >>> from hgclient import bprint, check, readchannel, runcommand
413 413 >>> @check
414 414 ... def phasesetscacheaftercommit(server):
415 415 ... readchannel(server)
416 416 ... # load _phasecache._phaserevs and _phasesets
417 417 ... runcommand(server, [b'log', b'-qr', b'draft()'])
418 418 ... # create draft commits by another process
419 419 ... for i in range(5, 7):
420 420 ... f = open('a', 'ab')
421 421 ... f.seek(0, os.SEEK_END)
422 422 ... f.write(b'a\n') and None
423 423 ... f.close()
424 424 ... os.system('hg commit -Aqm%d' % i)
425 425 ... # new commits should be listed as draft revisions
426 426 ... runcommand(server, [b'log', b'-qr', b'draft()'])
427 427 ... bprint(b'')
428 428 *** runcommand log -qr draft()
429 429 4:7966c8e3734d
430 430 *** runcommand log -qr draft()
431 431 4:7966c8e3734d
432 432 5:41f6602d1c4f
433 433 6:10501e202c35
434 434
435 435
436 436 >>> import os
437 437 >>> from hgclient import bprint, check, readchannel, runcommand
438 438 >>> @check
439 439 ... def phasesetscacheafterstrip(server):
440 440 ... readchannel(server)
441 441 ... # load _phasecache._phaserevs and _phasesets
442 442 ... runcommand(server, [b'log', b'-qr', b'draft()'])
443 443 ... # strip cached revisions by another process
444 444 ... os.system('hg --config extensions.strip= strip -q 5')
445 445 ... # shouldn't abort by "unknown revision '6'"
446 446 ... runcommand(server, [b'log', b'-qr', b'draft()'])
447 447 ... bprint(b'')
448 448 *** runcommand log -qr draft()
449 449 4:7966c8e3734d
450 450 5:41f6602d1c4f
451 451 6:10501e202c35
452 452 *** runcommand log -qr draft()
453 453 4:7966c8e3734d
454 454
455 455
456 456 cache of phase roots should be invalidated on strip (issue3827):
457 457
458 458 >>> import os
459 459 >>> from hgclient import check, readchannel, runcommand, sep
460 460 >>> @check
461 461 ... def phasecacheafterstrip(server):
462 462 ... readchannel(server)
463 463 ...
464 464 ... # create new head, 5:731265503d86
465 465 ... runcommand(server, [b'update', b'-C', b'0'])
466 466 ... f = open('a', 'ab')
467 467 ... f.write(b'a\n') and None
468 468 ... f.close()
469 469 ... runcommand(server, [b'commit', b'-Am.', b'a'])
470 470 ... runcommand(server, [b'log', b'-Gq'])
471 471 ...
472 472 ... # make it public; draft marker moves to 4:7966c8e3734d
473 473 ... runcommand(server, [b'phase', b'-p', b'.'])
474 474 ... # load _phasecache.phaseroots
475 475 ... runcommand(server, [b'phase', b'.'], outfilter=sep)
476 476 ...
477 477 ... # strip 1::4 outside server
478 478 ... os.system('hg -q --config extensions.mq= strip 1')
479 479 ...
480 480 ... # shouldn't raise "7966c8e3734d: no node!"
481 481 ... runcommand(server, [b'branches'])
482 482 *** runcommand update -C 0
483 483 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
484 484 (leaving bookmark bm3)
485 485 *** runcommand commit -Am. a
486 486 created new head
487 487 *** runcommand log -Gq
488 488 @ 5:731265503d86
489 489 |
490 490 | o 4:7966c8e3734d
491 491 | |
492 492 | o 3:b9b85890c400
493 493 | |
494 494 | o 2:aef17e88f5f0
495 495 | |
496 496 | o 1:d3a0a68be6de
497 497 |/
498 498 o 0:eff892de26ec
499 499
500 500 *** runcommand phase -p .
501 501 *** runcommand phase .
502 502 5: public
503 503 *** runcommand branches
504 504 default 1:731265503d86
505 505
506 506 in-memory cache must be reloaded if transaction is aborted. otherwise
507 507 changelog and manifest would have invalid node:
508 508
509 509 $ echo a >> a
510 510 >>> from hgclient import check, readchannel, runcommand
511 511 >>> @check
512 512 ... def txabort(server):
513 513 ... readchannel(server)
514 514 ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false',
515 515 ... b'-mfoo'])
516 516 ... runcommand(server, [b'verify'])
517 517 *** runcommand commit --config hooks.pretxncommit=false -mfoo
518 518 transaction abort!
519 519 rollback completed
520 520 abort: pretxncommit hook exited with status 1
521 521 [255]
522 522 *** runcommand verify
523 523 checking changesets
524 524 checking manifests
525 525 crosschecking files in changesets and manifests
526 526 checking files
527 527 checked 2 changesets with 2 changes to 1 files
528 528 $ hg revert --no-backup -aq
529 529
530 530 $ cat >> .hg/hgrc << EOF
531 531 > [experimental]
532 532 > evolution.createmarkers=True
533 533 > EOF
534 534
535 535 >>> import os
536 536 >>> from hgclient import check, readchannel, runcommand
537 537 >>> @check
538 538 ... def obsolete(server):
539 539 ... readchannel(server)
540 540 ...
541 541 ... runcommand(server, [b'up', b'null'])
542 542 ... runcommand(server, [b'phase', b'-df', b'tip'])
543 543 ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`'
544 544 ... if os.name == 'nt':
545 545 ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe
546 546 ... os.system(cmd)
547 547 ... runcommand(server, [b'log', b'--hidden'])
548 548 ... runcommand(server, [b'log'])
549 549 *** runcommand up null
550 550 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
551 551 *** runcommand phase -df tip
552 552 1 new obsolescence markers
553 553 obsoleted 1 changesets
554 554 *** runcommand log --hidden
555 555 changeset: 1:731265503d86
556 556 tag: tip
557 557 user: test
558 558 date: Thu Jan 01 00:00:00 1970 +0000
559 559 obsolete: pruned
560 560 summary: .
561 561
562 562 changeset: 0:eff892de26ec
563 563 bookmark: bm1
564 564 bookmark: bm2
565 565 bookmark: bm3
566 566 user: test
567 567 date: Thu Jan 01 00:00:00 1970 +0000
568 568 summary: 1
569 569
570 570 *** runcommand log
571 571 changeset: 0:eff892de26ec
572 572 bookmark: bm1
573 573 bookmark: bm2
574 574 bookmark: bm3
575 575 tag: tip
576 576 user: test
577 577 date: Thu Jan 01 00:00:00 1970 +0000
578 578 summary: 1
579 579
580 580
581 581 $ cat <<EOF >> .hg/hgrc
582 582 > [extensions]
583 583 > mq =
584 584 > EOF
585 585
586 586 >>> import os
587 587 >>> from hgclient import check, readchannel, runcommand
588 588 >>> @check
589 589 ... def mqoutsidechanges(server):
590 590 ... readchannel(server)
591 591 ...
592 592 ... # load repo.mq
593 593 ... runcommand(server, [b'qapplied'])
594 594 ... os.system('hg qnew 0.diff')
595 595 ... # repo.mq should be invalidated
596 596 ... runcommand(server, [b'qapplied'])
597 597 ...
598 598 ... runcommand(server, [b'qpop', b'--all'])
599 599 ... os.system('hg qqueue --create foo')
600 600 ... # repo.mq should be recreated to point to new queue
601 601 ... runcommand(server, [b'qqueue', b'--active'])
602 602 *** runcommand qapplied
603 603 *** runcommand qapplied
604 604 0.diff
605 605 *** runcommand qpop --all
606 606 popping 0.diff
607 607 patch queue now empty
608 608 *** runcommand qqueue --active
609 609 foo
610 610
611 611 $ cat <<'EOF' > ../dbgui.py
612 612 > import os
613 613 > import sys
614 614 > from mercurial import commands, registrar
615 615 > cmdtable = {}
616 616 > command = registrar.command(cmdtable)
617 617 > @command(b"debuggetpass", norepo=True)
618 618 > def debuggetpass(ui):
619 619 > ui.write(b"%s\n" % ui.getpass())
620 620 > @command(b"debugprompt", norepo=True)
621 621 > def debugprompt(ui):
622 622 > ui.write(b"%s\n" % ui.prompt(b"prompt:"))
623 623 > @command(b"debugpromptchoice", norepo=True)
624 624 > def debugpromptchoice(ui):
625 625 > msg = b"promptchoice (y/n)? $$ &Yes $$ &No"
626 626 > ui.write(b"%d\n" % ui.promptchoice(msg))
627 627 > @command(b"debugreadstdin", norepo=True)
628 628 > def debugreadstdin(ui):
629 629 > ui.write(b"read: %r\n" % sys.stdin.read(1))
630 630 > @command(b"debugwritestdout", norepo=True)
631 631 > def debugwritestdout(ui):
632 632 > os.write(1, b"low-level stdout fd and\n")
633 633 > sys.stdout.write("stdout should be redirected to stderr\n")
634 634 > sys.stdout.flush()
635 635 > EOF
636 636 $ cat <<EOF >> .hg/hgrc
637 637 > [extensions]
638 638 > dbgui = ../dbgui.py
639 639 > EOF
640 640
641 641 >>> from hgclient import check, readchannel, runcommand, stringio
642 642 >>> @check
643 643 ... def getpass(server):
644 644 ... readchannel(server)
645 645 ... runcommand(server, [b'debuggetpass', b'--config',
646 646 ... b'ui.interactive=True'],
647 647 ... input=stringio(b'1234\n'))
648 648 ... runcommand(server, [b'debuggetpass', b'--config',
649 649 ... b'ui.interactive=True'],
650 650 ... input=stringio(b'\n'))
651 651 ... runcommand(server, [b'debuggetpass', b'--config',
652 652 ... b'ui.interactive=True'],
653 653 ... input=stringio(b''))
654 654 ... runcommand(server, [b'debugprompt', b'--config',
655 655 ... b'ui.interactive=True'],
656 656 ... input=stringio(b'5678\n'))
657 ... runcommand(server, [b'debugprompt', b'--config',
658 ... b'ui.interactive=True'],
659 ... input=stringio(b'\nremainder\nshould\nnot\nbe\nread\n'))
657 660 ... runcommand(server, [b'debugreadstdin'])
658 661 ... runcommand(server, [b'debugwritestdout'])
659 662 *** runcommand debuggetpass --config ui.interactive=True
660 663 password: 1234
661 664 *** runcommand debuggetpass --config ui.interactive=True
662 665 password:
663 666 *** runcommand debuggetpass --config ui.interactive=True
664 667 password: abort: response expected
665 668 [255]
666 669 *** runcommand debugprompt --config ui.interactive=True
667 670 prompt: 5678
671 *** runcommand debugprompt --config ui.interactive=True
672 prompt: y
668 673 *** runcommand debugreadstdin
669 674 read: ''
670 675 *** runcommand debugwritestdout
671 676 low-level stdout fd and
672 677 stdout should be redirected to stderr
673 678
674 679
675 680 run commandserver in commandserver, which is silly but should work:
676 681
677 682 >>> from hgclient import bprint, check, readchannel, runcommand, stringio
678 683 >>> @check
679 684 ... def nested(server):
680 685 ... bprint(b'%c, %r' % readchannel(server))
681 686 ... class nestedserver(object):
682 687 ... stdin = stringio(b'getencoding\n')
683 688 ... stdout = stringio()
684 689 ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'],
685 690 ... output=nestedserver.stdout, input=nestedserver.stdin)
686 691 ... nestedserver.stdout.seek(0)
687 692 ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello
688 693 ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding
689 694 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
690 695 *** runcommand serve --cmdserver pipe
691 696 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
692 697 r, '*' (glob)
693 698
694 699
695 700 start without repository:
696 701
697 702 $ cd ..
698 703
699 704 >>> from hgclient import bprint, check, readchannel, runcommand
700 705 >>> @check
701 706 ... def hellomessage(server):
702 707 ... ch, data = readchannel(server)
703 708 ... bprint(b'%c, %r' % (ch, data))
704 709 ... # run an arbitrary command to make sure the next thing the server
705 710 ... # sends isn't part of the hello message
706 711 ... runcommand(server, [b'id'])
707 712 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
708 713 *** runcommand id
709 714 abort: there is no Mercurial repository here (.hg not found)
710 715 [255]
711 716
712 717 >>> from hgclient import check, readchannel, runcommand
713 718 >>> @check
714 719 ... def startwithoutrepo(server):
715 720 ... readchannel(server)
716 721 ... runcommand(server, [b'init', b'repo2'])
717 722 ... runcommand(server, [b'id', b'-R', b'repo2'])
718 723 *** runcommand init repo2
719 724 *** runcommand id -R repo2
720 725 000000000000 tip
721 726
722 727
723 728 don't fall back to cwd if invalid -R path is specified (issue4805):
724 729
725 730 $ cd repo
726 731 $ hg serve --cmdserver pipe -R ../nonexistent
727 732 abort: repository ../nonexistent not found!
728 733 [255]
729 734 $ cd ..
730 735
731 736
732 737 structured message channel:
733 738
734 739 $ cat <<'EOF' >> repo2/.hg/hgrc
735 740 > [ui]
736 741 > # server --config should precede repository option
737 742 > message-output = stdio
738 743 > EOF
739 744
740 745 >>> from hgclient import bprint, checkwith, readchannel, runcommand
741 746 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
742 747 ... b'--config', b'cmdserver.message-encodings=foo cbor'])
743 748 ... def verify(server):
744 749 ... _ch, data = readchannel(server)
745 750 ... bprint(data)
746 751 ... runcommand(server, [b'-R', b'repo2', b'verify'])
747 752 capabilities: getencoding runcommand
748 753 encoding: ascii
749 754 message-encoding: cbor
750 755 pid: * (glob)
751 756 pgid: * (glob) (no-windows !)
752 757 *** runcommand -R repo2 verify
753 758 message: '\xa2DdataTchecking changesets\nDtypeFstatus'
754 759 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
755 760 message: '\xa2DdataSchecking manifests\nDtypeFstatus'
756 761 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
757 762 message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus'
758 763 message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@'
759 764 message: '\xa2DdataOchecking files\nDtypeFstatus'
760 765 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
761 766 message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus'
762 767
763 768 >>> from hgclient import checkwith, readchannel, runcommand, stringio
764 769 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
765 770 ... b'--config', b'cmdserver.message-encodings=cbor',
766 771 ... b'--config', b'extensions.dbgui=dbgui.py'])
767 772 ... def prompt(server):
768 773 ... readchannel(server)
769 774 ... interactive = [b'--config', b'ui.interactive=True']
770 775 ... runcommand(server, [b'debuggetpass'] + interactive,
771 776 ... input=stringio(b'1234\n'))
772 777 ... runcommand(server, [b'debugprompt'] + interactive,
773 778 ... input=stringio(b'5678\n'))
774 779 ... runcommand(server, [b'debugpromptchoice'] + interactive,
775 780 ... input=stringio(b'n\n'))
776 781 *** runcommand debuggetpass --config ui.interactive=True
777 782 message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt'
778 783 1234
779 784 *** runcommand debugprompt --config ui.interactive=True
780 785 message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt'
781 786 5678
782 787 *** runcommand debugpromptchoice --config ui.interactive=True
783 788 message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt'
784 789 1
785 790
786 791 bad message encoding:
787 792
788 793 $ hg serve --cmdserver pipe --config ui.message-output=channel
789 794 abort: no supported message encodings:
790 795 [255]
791 796 $ hg serve --cmdserver pipe --config ui.message-output=channel \
792 797 > --config cmdserver.message-encodings='foo bar'
793 798 abort: no supported message encodings: foo bar
794 799 [255]
795 800
796 801 unix domain socket:
797 802
798 803 $ cd repo
799 804 $ hg update -q
800 805
801 806 #if unix-socket unix-permissions
802 807
803 808 >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver
804 809 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
805 810 >>> def hellomessage(conn):
806 811 ... ch, data = readchannel(conn)
807 812 ... bprint(b'%c, %r' % (ch, data))
808 813 ... runcommand(conn, [b'id'])
809 814 >>> check(hellomessage, server.connect)
810 815 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
811 816 *** runcommand id
812 817 eff892de26ec tip bm1/bm2/bm3
813 818 >>> def unknowncommand(conn):
814 819 ... readchannel(conn)
815 820 ... conn.stdin.write(b'unknowncommand\n')
816 821 >>> check(unknowncommand, server.connect) # error sent to server.log
817 822 >>> def serverinput(conn):
818 823 ... readchannel(conn)
819 824 ... patch = b"""
820 825 ... # HG changeset patch
821 826 ... # User test
822 827 ... # Date 0 0
823 828 ... 2
824 829 ...
825 830 ... diff -r eff892de26ec -r 1ed24be7e7a0 a
826 831 ... --- a/a
827 832 ... +++ b/a
828 833 ... @@ -1,1 +1,2 @@
829 834 ... 1
830 835 ... +2
831 836 ... """
832 837 ... runcommand(conn, [b'import', b'-'], input=stringio(patch))
833 838 ... runcommand(conn, [b'log', b'-rtip', b'-q'])
834 839 >>> check(serverinput, server.connect)
835 840 *** runcommand import -
836 841 applying patch from stdin
837 842 *** runcommand log -rtip -q
838 843 2:1ed24be7e7a0
839 844 >>> server.shutdown()
840 845
841 846 $ cat .hg/server.log
842 847 listening at .hg/server.sock
843 848 abort: unknown command unknowncommand
844 849 killed!
845 850 $ rm .hg/server.log
846 851
847 852 if server crashed before hello, traceback will be sent to 'e' channel as
848 853 last ditch:
849 854
850 855 $ cat <<'EOF' > ../earlycrasher.py
851 856 > from mercurial import commandserver, extensions
852 857 > def _serverequest(orig, ui, repo, conn, createcmdserver, prereposetups):
853 858 > def createcmdserver(*args, **kwargs):
854 859 > raise Exception('crash')
855 860 > return orig(ui, repo, conn, createcmdserver, prereposetups)
856 861 > def extsetup(ui):
857 862 > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest)
858 863 > EOF
859 864 $ cat <<EOF >> .hg/hgrc
860 865 > [extensions]
861 866 > earlycrasher = ../earlycrasher.py
862 867 > EOF
863 868 >>> from hgclient import bprint, check, readchannel, unixserver
864 869 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
865 870 >>> def earlycrash(conn):
866 871 ... while True:
867 872 ... try:
868 873 ... ch, data = readchannel(conn)
869 874 ... for l in data.splitlines(True):
870 875 ... if not l.startswith(b' '):
871 876 ... bprint(b'%c, %r' % (ch, l))
872 877 ... except EOFError:
873 878 ... break
874 879 >>> check(earlycrash, server.connect)
875 880 e, 'Traceback (most recent call last):\n'
876 881 e, 'Exception: crash\n'
877 882 >>> server.shutdown()
878 883
879 884 $ cat .hg/server.log | grep -v '^ '
880 885 listening at .hg/server.sock
881 886 Traceback (most recent call last):
882 887 Exception: crash
883 888 killed!
884 889 #endif
885 890 #if no-unix-socket
886 891
887 892 $ hg serve --cmdserver unix -a .hg/server.sock
888 893 abort: unsupported platform
889 894 [255]
890 895
891 896 #endif
892 897
893 898 $ cd ..
894 899
895 900 Test that accessing to invalid changelog cache is avoided at
896 901 subsequent operations even if repo object is reused even after failure
897 902 of transaction (see 0a7610758c42 also)
898 903
899 904 "hg log" after failure of transaction is needed to detect invalid
900 905 cache in repoview: this can't detect by "hg verify" only.
901 906
902 907 Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
903 908 4) are tested, because '00changelog.i' are differently changed in each
904 909 cases.
905 910
906 911 $ cat > $TESTTMP/failafterfinalize.py <<EOF
907 912 > # extension to abort transaction after finalization forcibly
908 913 > from mercurial import commands, error, extensions, lock as lockmod
909 914 > from mercurial import registrar
910 915 > cmdtable = {}
911 916 > command = registrar.command(cmdtable)
912 917 > configtable = {}
913 918 > configitem = registrar.configitem(configtable)
914 919 > configitem(b'failafterfinalize', b'fail',
915 920 > default=None,
916 921 > )
917 922 > def fail(tr):
918 923 > raise error.Abort(b'fail after finalization')
919 924 > def reposetup(ui, repo):
920 925 > class failrepo(repo.__class__):
921 926 > def commitctx(self, ctx, error=False, origctx=None):
922 927 > if self.ui.configbool(b'failafterfinalize', b'fail'):
923 928 > # 'sorted()' by ASCII code on category names causes
924 929 > # invoking 'fail' after finalization of changelog
925 930 > # using "'cl-%i' % id(self)" as category name
926 931 > self.currenttransaction().addfinalize(b'zzzzzzzz', fail)
927 932 > return super(failrepo, self).commitctx(ctx, error, origctx)
928 933 > repo.__class__ = failrepo
929 934 > EOF
930 935
931 936 $ hg init repo3
932 937 $ cd repo3
933 938
934 939 $ cat <<EOF >> $HGRCPATH
935 940 > [ui]
936 941 > logtemplate = {rev} {desc|firstline} ({files})\n
937 942 >
938 943 > [extensions]
939 944 > failafterfinalize = $TESTTMP/failafterfinalize.py
940 945 > EOF
941 946
942 947 - test failure with "empty changelog"
943 948
944 949 $ echo foo > foo
945 950 $ hg add foo
946 951
947 952 (failure before finalization)
948 953
949 954 >>> from hgclient import check, readchannel, runcommand
950 955 >>> @check
951 956 ... def abort(server):
952 957 ... readchannel(server)
953 958 ... runcommand(server, [b'commit',
954 959 ... b'--config', b'hooks.pretxncommit=false',
955 960 ... b'-mfoo'])
956 961 ... runcommand(server, [b'log'])
957 962 ... runcommand(server, [b'verify', b'-q'])
958 963 *** runcommand commit --config hooks.pretxncommit=false -mfoo
959 964 transaction abort!
960 965 rollback completed
961 966 abort: pretxncommit hook exited with status 1
962 967 [255]
963 968 *** runcommand log
964 969 *** runcommand verify -q
965 970
966 971 (failure after finalization)
967 972
968 973 >>> from hgclient import check, readchannel, runcommand
969 974 >>> @check
970 975 ... def abort(server):
971 976 ... readchannel(server)
972 977 ... runcommand(server, [b'commit',
973 978 ... b'--config', b'failafterfinalize.fail=true',
974 979 ... b'-mfoo'])
975 980 ... runcommand(server, [b'log'])
976 981 ... runcommand(server, [b'verify', b'-q'])
977 982 *** runcommand commit --config failafterfinalize.fail=true -mfoo
978 983 transaction abort!
979 984 rollback completed
980 985 abort: fail after finalization
981 986 [255]
982 987 *** runcommand log
983 988 *** runcommand verify -q
984 989
985 990 - test failure with "not-empty changelog"
986 991
987 992 $ echo bar > bar
988 993 $ hg add bar
989 994 $ hg commit -mbar bar
990 995
991 996 (failure before finalization)
992 997
993 998 >>> from hgclient import check, readchannel, runcommand
994 999 >>> @check
995 1000 ... def abort(server):
996 1001 ... readchannel(server)
997 1002 ... runcommand(server, [b'commit',
998 1003 ... b'--config', b'hooks.pretxncommit=false',
999 1004 ... b'-mfoo', b'foo'])
1000 1005 ... runcommand(server, [b'log'])
1001 1006 ... runcommand(server, [b'verify', b'-q'])
1002 1007 *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
1003 1008 transaction abort!
1004 1009 rollback completed
1005 1010 abort: pretxncommit hook exited with status 1
1006 1011 [255]
1007 1012 *** runcommand log
1008 1013 0 bar (bar)
1009 1014 *** runcommand verify -q
1010 1015
1011 1016 (failure after finalization)
1012 1017
1013 1018 >>> from hgclient import check, readchannel, runcommand
1014 1019 >>> @check
1015 1020 ... def abort(server):
1016 1021 ... readchannel(server)
1017 1022 ... runcommand(server, [b'commit',
1018 1023 ... b'--config', b'failafterfinalize.fail=true',
1019 1024 ... b'-mfoo', b'foo'])
1020 1025 ... runcommand(server, [b'log'])
1021 1026 ... runcommand(server, [b'verify', b'-q'])
1022 1027 *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
1023 1028 transaction abort!
1024 1029 rollback completed
1025 1030 abort: fail after finalization
1026 1031 [255]
1027 1032 *** runcommand log
1028 1033 0 bar (bar)
1029 1034 *** runcommand verify -q
1030 1035
1031 1036 $ cd ..
1032 1037
1033 1038 Test symlink traversal over cached audited paths:
1034 1039 -------------------------------------------------
1035 1040
1036 1041 #if symlink
1037 1042
1038 1043 set up symlink hell
1039 1044
1040 1045 $ mkdir merge-symlink-out
1041 1046 $ hg init merge-symlink
1042 1047 $ cd merge-symlink
1043 1048 $ touch base
1044 1049 $ hg commit -qAm base
1045 1050 $ ln -s ../merge-symlink-out a
1046 1051 $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
1047 1052 $ hg up -q 0
1048 1053 $ mkdir a
1049 1054 $ touch a/poisoned
1050 1055 $ hg commit -qAm 'file a/poisoned'
1051 1056 $ hg log -G -T '{rev}: {desc}\n'
1052 1057 @ 2: file a/poisoned
1053 1058 |
1054 1059 | o 1: symlink a -> ../merge-symlink-out
1055 1060 |/
1056 1061 o 0: base
1057 1062
1058 1063
1059 1064 try trivial merge after update: cache of audited paths should be discarded,
1060 1065 and the merge should fail (issue5628)
1061 1066
1062 1067 $ hg up -q null
1063 1068 >>> from hgclient import check, readchannel, runcommand
1064 1069 >>> @check
1065 1070 ... def merge(server):
1066 1071 ... readchannel(server)
1067 1072 ... # audit a/poisoned as a good path
1068 1073 ... runcommand(server, [b'up', b'-qC', b'2'])
1069 1074 ... runcommand(server, [b'up', b'-qC', b'1'])
1070 1075 ... # here a is a symlink, so a/poisoned is bad
1071 1076 ... runcommand(server, [b'merge', b'2'])
1072 1077 *** runcommand up -qC 2
1073 1078 *** runcommand up -qC 1
1074 1079 *** runcommand merge 2
1075 1080 abort: path 'a/poisoned' traverses symbolic link 'a'
1076 1081 [255]
1077 1082 $ ls ../merge-symlink-out
1078 1083
1079 1084 cache of repo.auditor should be discarded, so matcher would never traverse
1080 1085 symlinks:
1081 1086
1082 1087 $ hg up -qC 0
1083 1088 $ touch ../merge-symlink-out/poisoned
1084 1089 >>> from hgclient import check, readchannel, runcommand
1085 1090 >>> @check
1086 1091 ... def files(server):
1087 1092 ... readchannel(server)
1088 1093 ... runcommand(server, [b'up', b'-qC', b'2'])
1089 1094 ... # audit a/poisoned as a good path
1090 1095 ... runcommand(server, [b'files', b'a/poisoned'])
1091 1096 ... runcommand(server, [b'up', b'-qC', b'0'])
1092 1097 ... runcommand(server, [b'up', b'-qC', b'1'])
1093 1098 ... # here 'a' is a symlink, so a/poisoned should be warned
1094 1099 ... runcommand(server, [b'files', b'a/poisoned'])
1095 1100 *** runcommand up -qC 2
1096 1101 *** runcommand files a/poisoned
1097 1102 a/poisoned
1098 1103 *** runcommand up -qC 0
1099 1104 *** runcommand up -qC 1
1100 1105 *** runcommand files a/poisoned
1101 1106 abort: path 'a/poisoned' traverses symbolic link 'a'
1102 1107 [255]
1103 1108
1104 1109 $ cd ..
1105 1110
1106 1111 #endif
General Comments 0
You need to be logged in to leave comments. Login now