sapling/edenscm/hgext/sshaskpass.py
Jun Wu 6ad31bbf6a codemod: replace os.fdopen with util.fdopen
Summary:
`util.fdopen` now adds workarounds for read+write+seek files on Windows.
This should solve issues we have seen on Windows behaviors.

See https://www.mercurial-scm.org/repo/hg/rev/3686fa2b8eee for the Windows weirdness.

Here is a minimal program to reproduce the weirdness:

```
import os

f = open("a.txt", "wb+")

# Write 12 bytes
f.write(b"b" * 12)

# Read byte slice 2..5
f.seek(2, os.SEEK_SET)
data = f.read(3)

# Try SEEK_END
f.seek(0, os.SEEK_END)
print("%d (expect 12)" % f.tell())  # got 5 using some python.exe
```

Reviewed By: xavierd

Differential Revision: D16033678

fbshipit-source-id: 4f17c463d9bfcc0cdd38d1b15f2a9e38e5b4c132
2019-06-27 13:10:20 -07:00

274 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python
# sshaskpass.py
#
# Copyright 2016 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""ssh-askpass implementation that works with chg
chg runs ssh at server side, and ssh does not have access to /dev/tty thus
unable to ask for password interactively when its output is being piped (ex.
during hg push or pull).
When ssh is unable to use /dev/tty, it will try to run SSH_ASKPASS if DISPLAY
is set, which is usually a GUI program. Here we set it to a special program
receiving fds from a simple unix socket server.
This file is both a mercurial extension to start that tty server and a
standalone ssh-askpass script.
"""
# Attention: Do NOT import anything inside mercurial here. This file also runs
# standalone without the mercurial environment, in which case it cannot import
# mercurial modules correctly.
import contextlib
import os
import signal
import socket
import sys
import tempfile
from multiprocessing.reduction import recv_handle, send_handle
try:
from edenscm.mercurial import encoding
environ = encoding.environ
except ImportError:
environ = getattr(os, "environ")
# backup tty fds. useful if we lose them later, like chg starting the pager
_ttyfds = []
@contextlib.contextmanager
def _silentexception(terminate=False):
"""silent common exceptions
useful if we don't want to pollute the terminal
exit if terminal is True
"""
exitcode = 0
try:
yield
except KeyboardInterrupt:
exitcode = 1
except Exception:
exitcode = 2
if terminate:
os._exit(exitcode)
def _sockbind(sock, addr):
"""shim for util.bindunixsocket"""
sock.bind(addr)
def _isttyserverneeded():
# respect user's setting, SSH_ASKPASS is likely a gui program
if "SSH_ASKPASS" in environ:
return False
# the tty server is not needed if /dev/tty can be opened
try:
with open("/dev/tty"):
return False
except Exception:
pass
# if no backup tty fds, and neither stdin nor stderr are tty, give up
if not _ttyfds and not all(f.isatty() for f in [sys.stdin, sys.stderr]):
return False
# tty server is needed
return True
def _startttyserver():
"""start a tty fd server
the server will send tty read and write fds via unix socket
listens at sockpath: $TMPDIR/ttysrv$UID/$PID
returns (pid, sockpath)
"""
sockpath = os.path.join(tempfile.mkdtemp("ttysrv"), str(os.getpid()))
pipes = os.pipe()
pid = os.fork()
if pid:
# parent, wait for the child to start listening
os.close(pipes[1])
os.read(pipes[0], 1)
os.close(pipes[0])
return pid, sockpath
# child, starts the server
ttyrfd, ttywfd = _ttyfds or [sys.stdin.fileno(), sys.stderr.fileno()]
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
getattr(util, "bindunixsocket", _sockbind)(sock, sockpath)
sock.listen(1)
# unblock parent
os.close(pipes[0])
os.write(pipes[1], " ")
os.close(pipes[1])
with _silentexception(terminate=True):
while True:
conn, addr = sock.accept()
# 0: a dummy destination_pid, is ignored on posix systems
send_handle(conn, ttyrfd, 0)
send_handle(conn, ttywfd, 0)
conn.close()
def _killprocess(pid):
"""kill and reap a child process"""
os.kill(pid, signal.SIGTERM)
try:
os.waitpid(pid, 0)
except KeyboardInterrupt:
pass
except Exception:
pass
def _receivefds(sockpath):
"""get fds from the tty server listening at sockpath
returns (readfd, writefd)
"""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# use chdir to handle long sockpath
os.chdir(os.path.dirname(sockpath) or ".")
sock.connect(os.path.basename(sockpath))
rfd = recv_handle(sock)
wfd = recv_handle(sock)
return (rfd, wfd)
def _validaterepo(orig, self, sshcmd, args, remotecmd, sshenv=None):
if not _isttyserverneeded():
return orig(self, sshcmd, args, remotecmd, sshenv=sshenv)
pid = sockpath = scriptpath = None
with _silentexception(terminate=False):
pid, sockpath = _startttyserver()
scriptpath = sockpath + ".sh"
with open(scriptpath, "w") as f:
f.write(
'#!/bin/bash\nexec %s %s "$@"'
% (util.shellquote("python2"), util.shellquote(__file__))
)
os.chmod(scriptpath, 0o755)
env = {
# ssh will not use SSH_ASKPASS if DISPLAY is not set
"DISPLAY": environ.get("DISPLAY", ""),
"SSH_ASKPASS": util.shellquote(scriptpath),
"TTYSOCK": util.shellquote(sockpath),
}
prefix = " ".join("%s=%s" % (k, v) for k, v in env.items())
# modify sshcmd to include new environ
sshcmd = "%s %s" % (prefix, sshcmd)
try:
return orig(self, sshcmd, args, remotecmd, sshenv=sshenv)
finally:
if pid:
_killprocess(pid)
for path in [scriptpath, sockpath]:
if path and os.path.exists(path):
util.unlinkpath(path, ignoremissing=True)
def _attachio(orig, self):
orig(self)
# backup read, write tty fds to _ttyfds
if _ttyfds:
return
ui = self.ui
if ui.fin.isatty() and ui.ferr.isatty():
rfd = os.dup(ui.fin.fileno())
wfd = os.dup(ui.ferr.fileno())
_ttyfds[:] = [rfd, wfd]
def _patchchgserver():
"""patch chgserver so we can backup tty fds before they are replaced if
chg starts the pager.
"""
chgserver = None
try:
from edenscm.mercurial import chgserver
except ImportError:
try:
chgserver = extensions.find("chgserver")
except KeyError:
pass
server = getattr(chgserver, "chgcmdserver", None)
if server and "attachio" in server.capabilities:
orig = server.attachio
server.capabilities["attachio"] = extensions.bind(_attachio, orig)
def uisetup(ui):
# _validaterepo runs ssh and needs to be wrapped
extensions.wrapfunction(sshpeer.sshpeer, "_validaterepo", _validaterepo)
_patchchgserver()
def _setecho(ttyr, enableecho):
import termios
attrs = termios.tcgetattr(ttyr)
if bool(enableecho) == bool(attrs[3] & termios.ECHO):
return
attrs[3] ^= termios.ECHO
termios.tcsetattr(ttyr, termios.TCSANOW, attrs)
def _shoulddisableecho(prompt):
# we don't have the "flag" information from openssh's
# read_passphrase(const char *prompt, int flags).
# guess from the prompt string.
# do not match "Passcode or option"
if "Passcode or option" in prompt:
return False
# match "password", "Password", "passphrase", "Passphrase".
return prompt.find("ass") >= 0
def _sshaskpassmain(prompt):
"""the ssh-askpass client"""
rfd, wfd = _receivefds(environ["TTYSOCK"])
# cannot use util.fdopen here - the script runs outside "edenscm" context.
r, w = os.fdopen(rfd, "r"), os.fdopen(wfd, "a")
w.write("\033[31;1m==== AUTHENTICATING FOR SSH ====\033[0m\n")
w.write(prompt)
w.flush()
shouldecho = not _shoulddisableecho(prompt)
_setecho(r, shouldecho)
try:
line = r.readline()
finally:
if not shouldecho:
w.write("\n")
_setecho(r, True)
sys.stdout.write(line)
sys.stdout.flush()
w.write("\033[31;1m==== AUTHENTICATION COMPLETE ====\033[0m\n")
if __name__ == "__main__" and all(n in environ for n in ["SSH_ASKPASS", "TTYSOCK"]):
# started by ssh as ssh-askpass
with _silentexception(terminate=True):
_sshaskpassmain(" ".join(sys.argv[1:]))
else:
# imported as a mercurial extension
from edenscm.mercurial import extensions, sshpeer, util