##// END OF EJS Templates
py3: make stdout line-buffered if connected to a TTY...
Manuel Jacob -
r45477:f9734b2d default
parent child Browse files
Show More
@@ -1,677 +1,701 b''
1 1 # procutil.py - utility for managing processes and executable environment
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import contextlib
13 13 import errno
14 14 import io
15 15 import os
16 16 import signal
17 17 import subprocess
18 18 import sys
19 19 import threading
20 20 import time
21 21
22 22 from ..i18n import _
23 23 from ..pycompat import (
24 24 getattr,
25 25 open,
26 26 )
27 27
28 28 from .. import (
29 29 encoding,
30 30 error,
31 31 policy,
32 32 pycompat,
33 33 )
34 34
35 35 # Import like this to keep import-checker happy
36 36 from ..utils import resourceutil
37 37
38 38 osutil = policy.importmod('osutil')
39 39
40 40 stderr = pycompat.stderr
41 41 stdin = pycompat.stdin
42 42 stdout = pycompat.stdout
43 43
44 44
45 45 def isatty(fp):
46 46 try:
47 47 return fp.isatty()
48 48 except AttributeError:
49 49 return False
50 50
51 51
52 if pycompat.ispy3:
53
54 class LineBufferedWrapper(object):
55 def __init__(self, orig):
56 self.orig = orig
57
58 def __getattr__(self, attr):
59 return getattr(self.orig, attr)
60
61 def write(self, s):
62 orig = self.orig
63 res = orig.write(s)
64 if s.endswith(b'\n'):
65 orig.flush()
66 return res
67
68 io.BufferedIOBase.register(LineBufferedWrapper)
69
70
52 71 # glibc determines buffering on first write to stdout - if we replace a TTY
53 72 # destined stdout with a pipe destined stdout (e.g. pager), we want line
54 73 # buffering (or unbuffered, on Windows)
55 74 if isatty(stdout):
56 75 if pycompat.iswindows:
57 76 # Windows doesn't support line buffering
58 77 stdout = os.fdopen(stdout.fileno(), 'wb', 0)
59 elif not pycompat.ispy3:
60 # on Python 3, stdout (sys.stdout.buffer) is already line buffered and
61 # buffering=1 is not handled in binary mode
78 elif pycompat.ispy3:
79 # On Python 3, buffered binary streams can't be set line-buffered.
80 # Therefore we have a wrapper that implements line buffering.
81 if isinstance(stdout, io.BufferedIOBase) and not isinstance(
82 stdout, LineBufferedWrapper
83 ):
84 stdout = LineBufferedWrapper(stdout)
85 else:
62 86 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
63 87
64 88 if pycompat.iswindows:
65 89 from .. import windows as platform
66 90
67 91 stdout = platform.winstdout(stdout)
68 92 else:
69 93 from .. import posix as platform
70 94
71 95 findexe = platform.findexe
72 96 _gethgcmd = platform.gethgcmd
73 97 getuser = platform.getuser
74 98 getpid = os.getpid
75 99 hidewindow = platform.hidewindow
76 100 readpipe = platform.readpipe
77 101 setbinary = platform.setbinary
78 102 setsignalhandler = platform.setsignalhandler
79 103 shellquote = platform.shellquote
80 104 shellsplit = platform.shellsplit
81 105 spawndetached = platform.spawndetached
82 106 sshargs = platform.sshargs
83 107 testpid = platform.testpid
84 108
85 109 try:
86 110 setprocname = osutil.setprocname
87 111 except AttributeError:
88 112 pass
89 113 try:
90 114 unblocksignal = osutil.unblocksignal
91 115 except AttributeError:
92 116 pass
93 117
94 118 closefds = pycompat.isposix
95 119
96 120
97 121 def explainexit(code):
98 122 """return a message describing a subprocess status
99 123 (codes from kill are negative - not os.system/wait encoding)"""
100 124 if code >= 0:
101 125 return _(b"exited with status %d") % code
102 126 return _(b"killed by signal %d") % -code
103 127
104 128
105 129 class _pfile(object):
106 130 """File-like wrapper for a stream opened by subprocess.Popen()"""
107 131
108 132 def __init__(self, proc, fp):
109 133 self._proc = proc
110 134 self._fp = fp
111 135
112 136 def close(self):
113 137 # unlike os.popen(), this returns an integer in subprocess coding
114 138 self._fp.close()
115 139 return self._proc.wait()
116 140
117 141 def __iter__(self):
118 142 return iter(self._fp)
119 143
120 144 def __getattr__(self, attr):
121 145 return getattr(self._fp, attr)
122 146
123 147 def __enter__(self):
124 148 return self
125 149
126 150 def __exit__(self, exc_type, exc_value, exc_tb):
127 151 self.close()
128 152
129 153
130 154 def popen(cmd, mode=b'rb', bufsize=-1):
131 155 if mode == b'rb':
132 156 return _popenreader(cmd, bufsize)
133 157 elif mode == b'wb':
134 158 return _popenwriter(cmd, bufsize)
135 159 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
136 160
137 161
138 162 def _popenreader(cmd, bufsize):
139 163 p = subprocess.Popen(
140 164 tonativestr(cmd),
141 165 shell=True,
142 166 bufsize=bufsize,
143 167 close_fds=closefds,
144 168 stdout=subprocess.PIPE,
145 169 )
146 170 return _pfile(p, p.stdout)
147 171
148 172
149 173 def _popenwriter(cmd, bufsize):
150 174 p = subprocess.Popen(
151 175 tonativestr(cmd),
152 176 shell=True,
153 177 bufsize=bufsize,
154 178 close_fds=closefds,
155 179 stdin=subprocess.PIPE,
156 180 )
157 181 return _pfile(p, p.stdin)
158 182
159 183
160 184 def popen2(cmd, env=None):
161 185 # Setting bufsize to -1 lets the system decide the buffer size.
162 186 # The default for bufsize is 0, meaning unbuffered. This leads to
163 187 # poor performance on Mac OS X: http://bugs.python.org/issue4194
164 188 p = subprocess.Popen(
165 189 tonativestr(cmd),
166 190 shell=True,
167 191 bufsize=-1,
168 192 close_fds=closefds,
169 193 stdin=subprocess.PIPE,
170 194 stdout=subprocess.PIPE,
171 195 env=tonativeenv(env),
172 196 )
173 197 return p.stdin, p.stdout
174 198
175 199
176 200 def popen3(cmd, env=None):
177 201 stdin, stdout, stderr, p = popen4(cmd, env)
178 202 return stdin, stdout, stderr
179 203
180 204
181 205 def popen4(cmd, env=None, bufsize=-1):
182 206 p = subprocess.Popen(
183 207 tonativestr(cmd),
184 208 shell=True,
185 209 bufsize=bufsize,
186 210 close_fds=closefds,
187 211 stdin=subprocess.PIPE,
188 212 stdout=subprocess.PIPE,
189 213 stderr=subprocess.PIPE,
190 214 env=tonativeenv(env),
191 215 )
192 216 return p.stdin, p.stdout, p.stderr, p
193 217
194 218
195 219 def pipefilter(s, cmd):
196 220 '''filter string S through command CMD, returning its output'''
197 221 p = subprocess.Popen(
198 222 tonativestr(cmd),
199 223 shell=True,
200 224 close_fds=closefds,
201 225 stdin=subprocess.PIPE,
202 226 stdout=subprocess.PIPE,
203 227 )
204 228 pout, perr = p.communicate(s)
205 229 return pout
206 230
207 231
208 232 def tempfilter(s, cmd):
209 233 '''filter string S through a pair of temporary files with CMD.
210 234 CMD is used as a template to create the real command to be run,
211 235 with the strings INFILE and OUTFILE replaced by the real names of
212 236 the temporary files generated.'''
213 237 inname, outname = None, None
214 238 try:
215 239 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
216 240 fp = os.fdopen(infd, 'wb')
217 241 fp.write(s)
218 242 fp.close()
219 243 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
220 244 os.close(outfd)
221 245 cmd = cmd.replace(b'INFILE', inname)
222 246 cmd = cmd.replace(b'OUTFILE', outname)
223 247 code = system(cmd)
224 248 if pycompat.sysplatform == b'OpenVMS' and code & 1:
225 249 code = 0
226 250 if code:
227 251 raise error.Abort(
228 252 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
229 253 )
230 254 with open(outname, b'rb') as fp:
231 255 return fp.read()
232 256 finally:
233 257 try:
234 258 if inname:
235 259 os.unlink(inname)
236 260 except OSError:
237 261 pass
238 262 try:
239 263 if outname:
240 264 os.unlink(outname)
241 265 except OSError:
242 266 pass
243 267
244 268
245 269 _filtertable = {
246 270 b'tempfile:': tempfilter,
247 271 b'pipe:': pipefilter,
248 272 }
249 273
250 274
251 275 def filter(s, cmd):
252 276 """filter a string through a command that transforms its input to its
253 277 output"""
254 278 for name, fn in pycompat.iteritems(_filtertable):
255 279 if cmd.startswith(name):
256 280 return fn(s, cmd[len(name) :].lstrip())
257 281 return pipefilter(s, cmd)
258 282
259 283
260 284 _hgexecutable = None
261 285
262 286
263 287 def hgexecutable():
264 288 """return location of the 'hg' executable.
265 289
266 290 Defaults to $HG or 'hg' in the search path.
267 291 """
268 292 if _hgexecutable is None:
269 293 hg = encoding.environ.get(b'HG')
270 294 mainmod = sys.modules['__main__']
271 295 if hg:
272 296 _sethgexecutable(hg)
273 297 elif resourceutil.mainfrozen():
274 298 if getattr(sys, 'frozen', None) == 'macosx_app':
275 299 # Env variable set by py2app
276 300 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
277 301 else:
278 302 _sethgexecutable(pycompat.sysexecutable)
279 303 elif (
280 304 not pycompat.iswindows
281 305 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
282 306 ):
283 307 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
284 308 else:
285 309 _sethgexecutable(
286 310 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
287 311 )
288 312 return _hgexecutable
289 313
290 314
291 315 def _sethgexecutable(path):
292 316 """set location of the 'hg' executable"""
293 317 global _hgexecutable
294 318 _hgexecutable = path
295 319
296 320
297 321 def _testfileno(f, stdf):
298 322 fileno = getattr(f, 'fileno', None)
299 323 try:
300 324 return fileno and fileno() == stdf.fileno()
301 325 except io.UnsupportedOperation:
302 326 return False # fileno() raised UnsupportedOperation
303 327
304 328
305 329 def isstdin(f):
306 330 return _testfileno(f, sys.__stdin__)
307 331
308 332
309 333 def isstdout(f):
310 334 return _testfileno(f, sys.__stdout__)
311 335
312 336
313 337 def protectstdio(uin, uout):
314 338 """Duplicate streams and redirect original if (uin, uout) are stdio
315 339
316 340 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
317 341 redirected to stderr so the output is still readable.
318 342
319 343 Returns (fin, fout) which point to the original (uin, uout) fds, but
320 344 may be copy of (uin, uout). The returned streams can be considered
321 345 "owned" in that print(), exec(), etc. never reach to them.
322 346 """
323 347 uout.flush()
324 348 fin, fout = uin, uout
325 349 if _testfileno(uin, stdin):
326 350 newfd = os.dup(uin.fileno())
327 351 nullfd = os.open(os.devnull, os.O_RDONLY)
328 352 os.dup2(nullfd, uin.fileno())
329 353 os.close(nullfd)
330 354 fin = os.fdopen(newfd, 'rb')
331 355 if _testfileno(uout, stdout):
332 356 newfd = os.dup(uout.fileno())
333 357 os.dup2(stderr.fileno(), uout.fileno())
334 358 fout = os.fdopen(newfd, 'wb')
335 359 return fin, fout
336 360
337 361
338 362 def restorestdio(uin, uout, fin, fout):
339 363 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
340 364 uout.flush()
341 365 for f, uif in [(fin, uin), (fout, uout)]:
342 366 if f is not uif:
343 367 os.dup2(f.fileno(), uif.fileno())
344 368 f.close()
345 369
346 370
347 371 def shellenviron(environ=None):
348 372 """return environ with optional override, useful for shelling out"""
349 373
350 374 def py2shell(val):
351 375 """convert python object into string that is useful to shell"""
352 376 if val is None or val is False:
353 377 return b'0'
354 378 if val is True:
355 379 return b'1'
356 380 return pycompat.bytestr(val)
357 381
358 382 env = dict(encoding.environ)
359 383 if environ:
360 384 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
361 385 env[b'HG'] = hgexecutable()
362 386 return env
363 387
364 388
365 389 if pycompat.iswindows:
366 390
367 391 def shelltonative(cmd, env):
368 392 return platform.shelltocmdexe( # pytype: disable=module-attr
369 393 cmd, shellenviron(env)
370 394 )
371 395
372 396 tonativestr = encoding.strfromlocal
373 397 else:
374 398
375 399 def shelltonative(cmd, env):
376 400 return cmd
377 401
378 402 tonativestr = pycompat.identity
379 403
380 404
381 405 def tonativeenv(env):
382 406 '''convert the environment from bytes to strings suitable for Popen(), etc.
383 407 '''
384 408 return pycompat.rapply(tonativestr, env)
385 409
386 410
387 411 def system(cmd, environ=None, cwd=None, out=None):
388 412 '''enhanced shell command execution.
389 413 run with environment maybe modified, maybe in different dir.
390 414
391 415 if out is specified, it is assumed to be a file-like object that has a
392 416 write() method. stdout and stderr will be redirected to out.'''
393 417 try:
394 418 stdout.flush()
395 419 except Exception:
396 420 pass
397 421 env = shellenviron(environ)
398 422 if out is None or isstdout(out):
399 423 rc = subprocess.call(
400 424 tonativestr(cmd),
401 425 shell=True,
402 426 close_fds=closefds,
403 427 env=tonativeenv(env),
404 428 cwd=pycompat.rapply(tonativestr, cwd),
405 429 )
406 430 else:
407 431 proc = subprocess.Popen(
408 432 tonativestr(cmd),
409 433 shell=True,
410 434 close_fds=closefds,
411 435 env=tonativeenv(env),
412 436 cwd=pycompat.rapply(tonativestr, cwd),
413 437 stdout=subprocess.PIPE,
414 438 stderr=subprocess.STDOUT,
415 439 )
416 440 for line in iter(proc.stdout.readline, b''):
417 441 out.write(line)
418 442 proc.wait()
419 443 rc = proc.returncode
420 444 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
421 445 rc = 0
422 446 return rc
423 447
424 448
425 449 _is_gui = None
426 450
427 451
428 452 def _gui():
429 453 '''Are we running in a GUI?'''
430 454 if pycompat.isdarwin:
431 455 if b'SSH_CONNECTION' in encoding.environ:
432 456 # handle SSH access to a box where the user is logged in
433 457 return False
434 458 elif getattr(osutil, 'isgui', None):
435 459 # check if a CoreGraphics session is available
436 460 return osutil.isgui()
437 461 else:
438 462 # pure build; use a safe default
439 463 return True
440 464 else:
441 465 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
442 466
443 467
444 468 def gui():
445 469 global _is_gui
446 470 if _is_gui is None:
447 471 _is_gui = _gui()
448 472 return _is_gui
449 473
450 474
451 475 def hgcmd():
452 476 """Return the command used to execute current hg
453 477
454 478 This is different from hgexecutable() because on Windows we want
455 479 to avoid things opening new shell windows like batch files, so we
456 480 get either the python call or current executable.
457 481 """
458 482 if resourceutil.mainfrozen():
459 483 if getattr(sys, 'frozen', None) == 'macosx_app':
460 484 # Env variable set by py2app
461 485 return [encoding.environ[b'EXECUTABLEPATH']]
462 486 else:
463 487 return [pycompat.sysexecutable]
464 488 return _gethgcmd()
465 489
466 490
467 491 def rundetached(args, condfn):
468 492 """Execute the argument list in a detached process.
469 493
470 494 condfn is a callable which is called repeatedly and should return
471 495 True once the child process is known to have started successfully.
472 496 At this point, the child process PID is returned. If the child
473 497 process fails to start or finishes before condfn() evaluates to
474 498 True, return -1.
475 499 """
476 500 # Windows case is easier because the child process is either
477 501 # successfully starting and validating the condition or exiting
478 502 # on failure. We just poll on its PID. On Unix, if the child
479 503 # process fails to start, it will be left in a zombie state until
480 504 # the parent wait on it, which we cannot do since we expect a long
481 505 # running process on success. Instead we listen for SIGCHLD telling
482 506 # us our child process terminated.
483 507 terminated = set()
484 508
485 509 def handler(signum, frame):
486 510 terminated.add(os.wait())
487 511
488 512 prevhandler = None
489 513 SIGCHLD = getattr(signal, 'SIGCHLD', None)
490 514 if SIGCHLD is not None:
491 515 prevhandler = signal.signal(SIGCHLD, handler)
492 516 try:
493 517 pid = spawndetached(args)
494 518 while not condfn():
495 519 if (pid in terminated or not testpid(pid)) and not condfn():
496 520 return -1
497 521 time.sleep(0.1)
498 522 return pid
499 523 finally:
500 524 if prevhandler is not None:
501 525 signal.signal(signal.SIGCHLD, prevhandler)
502 526
503 527
504 528 @contextlib.contextmanager
505 529 def uninterruptible(warn):
506 530 """Inhibit SIGINT handling on a region of code.
507 531
508 532 Note that if this is called in a non-main thread, it turns into a no-op.
509 533
510 534 Args:
511 535 warn: A callable which takes no arguments, and returns True if the
512 536 previous signal handling should be restored.
513 537 """
514 538
515 539 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
516 540 shouldbail = []
517 541
518 542 def disabledsiginthandler(*args):
519 543 if warn():
520 544 signal.signal(signal.SIGINT, oldsiginthandler[0])
521 545 del oldsiginthandler[0]
522 546 shouldbail.append(True)
523 547
524 548 try:
525 549 try:
526 550 signal.signal(signal.SIGINT, disabledsiginthandler)
527 551 except ValueError:
528 552 # wrong thread, oh well, we tried
529 553 del oldsiginthandler[0]
530 554 yield
531 555 finally:
532 556 if oldsiginthandler:
533 557 signal.signal(signal.SIGINT, oldsiginthandler[0])
534 558 if shouldbail:
535 559 raise KeyboardInterrupt
536 560
537 561
538 562 if pycompat.iswindows:
539 563 # no fork on Windows, but we can create a detached process
540 564 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
541 565 # No stdlib constant exists for this value
542 566 DETACHED_PROCESS = 0x00000008
543 567 # Following creation flags might create a console GUI window.
544 568 # Using subprocess.CREATE_NEW_CONSOLE might helps.
545 569 # See https://phab.mercurial-scm.org/D1701 for discussion
546 570 _creationflags = (
547 571 DETACHED_PROCESS
548 572 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
549 573 )
550 574
551 575 def runbgcommand(
552 576 script,
553 577 env,
554 578 shell=False,
555 579 stdout=None,
556 580 stderr=None,
557 581 ensurestart=True,
558 582 record_wait=None,
559 583 ):
560 584 '''Spawn a command without waiting for it to finish.'''
561 585 # we can't use close_fds *and* redirect stdin. I'm not sure that we
562 586 # need to because the detached process has no console connection.
563 587 p = subprocess.Popen(
564 588 tonativestr(script),
565 589 shell=shell,
566 590 env=tonativeenv(env),
567 591 close_fds=True,
568 592 creationflags=_creationflags,
569 593 stdout=stdout,
570 594 stderr=stderr,
571 595 )
572 596 if record_wait is not None:
573 597 record_wait(p.wait)
574 598
575 599
576 600 else:
577 601
578 602 def runbgcommand(
579 603 cmd,
580 604 env,
581 605 shell=False,
582 606 stdout=None,
583 607 stderr=None,
584 608 ensurestart=True,
585 609 record_wait=None,
586 610 ):
587 611 '''Spawn a command without waiting for it to finish.
588 612
589 613
590 614 When `record_wait` is not None, the spawned process will not be fully
591 615 detached and the `record_wait` argument will be called with a the
592 616 `Subprocess.wait` function for the spawned process. This is mostly
593 617 useful for developers that need to make sure the spawned process
594 618 finished before a certain point. (eg: writing test)'''
595 619 if pycompat.isdarwin:
596 620 # avoid crash in CoreFoundation in case another thread
597 621 # calls gui() while we're calling fork().
598 622 gui()
599 623
600 624 # double-fork to completely detach from the parent process
601 625 # based on http://code.activestate.com/recipes/278731
602 626 if record_wait is None:
603 627 pid = os.fork()
604 628 if pid:
605 629 if not ensurestart:
606 630 # Even though we're not waiting on the child process,
607 631 # we still must call waitpid() on it at some point so
608 632 # it's not a zombie/defunct. This is especially relevant for
609 633 # chg since the parent process won't die anytime soon.
610 634 # We use a thread to make the overhead tiny.
611 635 def _do_wait():
612 636 os.waitpid(pid, 0)
613 637
614 638 threading.Thread(target=_do_wait, daemon=True).start()
615 639 return
616 640 # Parent process
617 641 (_pid, status) = os.waitpid(pid, 0)
618 642 if os.WIFEXITED(status):
619 643 returncode = os.WEXITSTATUS(status)
620 644 else:
621 645 returncode = -(os.WTERMSIG(status))
622 646 if returncode != 0:
623 647 # The child process's return code is 0 on success, an errno
624 648 # value on failure, or 255 if we don't have a valid errno
625 649 # value.
626 650 #
627 651 # (It would be slightly nicer to return the full exception info
628 652 # over a pipe as the subprocess module does. For now it
629 653 # doesn't seem worth adding that complexity here, though.)
630 654 if returncode == 255:
631 655 returncode = errno.EINVAL
632 656 raise OSError(
633 657 returncode,
634 658 b'error running %r: %s'
635 659 % (cmd, os.strerror(returncode)),
636 660 )
637 661 return
638 662
639 663 returncode = 255
640 664 try:
641 665 if record_wait is None:
642 666 # Start a new session
643 667 os.setsid()
644 668
645 669 stdin = open(os.devnull, b'r')
646 670 if stdout is None:
647 671 stdout = open(os.devnull, b'w')
648 672 if stderr is None:
649 673 stderr = open(os.devnull, b'w')
650 674
651 675 # connect stdin to devnull to make sure the subprocess can't
652 676 # muck up that stream for mercurial.
653 677 p = subprocess.Popen(
654 678 cmd,
655 679 shell=shell,
656 680 env=env,
657 681 close_fds=True,
658 682 stdin=stdin,
659 683 stdout=stdout,
660 684 stderr=stderr,
661 685 )
662 686 if record_wait is not None:
663 687 record_wait(p.wait)
664 688 returncode = 0
665 689 except EnvironmentError as ex:
666 690 returncode = ex.errno & 0xFF
667 691 if returncode == 0:
668 692 # This shouldn't happen, but just in case make sure the
669 693 # return code is never 0 here.
670 694 returncode = 255
671 695 except Exception:
672 696 returncode = 255
673 697 finally:
674 698 # mission accomplished, this child needs to exit and not
675 699 # continue the hg process here.
676 700 if record_wait is None:
677 701 os._exit(returncode)
General Comments 0
You need to be logged in to leave comments. Login now