mirror of
https://chromium.googlesource.com/chromium/tools/depot_tools.git
synced 2026-01-11 18:51:29 +00:00
[git-sso] Read sso helper output line by line
On Windows, the subprocess running the sso helper does not complete the `read()` in time. This CL reads the output line by line, and stops reading once the required fields have been specified. Bug: b/360206460 Change-Id: Ib5f95093cd6f9bbbe5093a7e16393ecd97934cf2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/5992110 Reviewed-by: Yiwei Zhang <yiwzhang@google.com> Commit-Queue: Anne Redulla <aredulla@google.com>
This commit is contained in:
@@ -352,6 +352,11 @@ class SSOAuthenticator(_Authenticator):
|
||||
# Overridden in tests.
|
||||
_timeout_secs = 5
|
||||
|
||||
# The required fields for the sso config, expected from the sso helper's
|
||||
# output.
|
||||
_required_fields = frozenset(
|
||||
['http.cookiefile', 'http.proxy', 'include.path'])
|
||||
|
||||
@dataclass
|
||||
class SSOInfo:
|
||||
proxy: httplib2.ProxyInfo
|
||||
@@ -387,24 +392,27 @@ class SSOAuthenticator(_Authenticator):
|
||||
return email.endswith('@google.com')
|
||||
|
||||
@classmethod
|
||||
def _parse_config(cls, config: str) -> SSOInfo:
|
||||
parsed: Dict[str, str] = dict(line.strip().split('=', 1)
|
||||
for line in config.splitlines())
|
||||
def _parse_config(cls, config: Dict[str, str]) -> SSOInfo:
|
||||
"""Parses the sso info from the given config.
|
||||
|
||||
Note: update cls._required_fields appropriately when making
|
||||
changes to this method, to ensure the field values are captured
|
||||
from the sso helper.
|
||||
"""
|
||||
fullAuthHeader = cast(
|
||||
str,
|
||||
scm.GIT.Capture([
|
||||
'config',
|
||||
'-f',
|
||||
parsed['include.path'],
|
||||
config['include.path'],
|
||||
'http.extraHeader',
|
||||
]))
|
||||
headerKey, headerValue = fullAuthHeader.split(':', 1)
|
||||
headers = {headerKey.strip(): headerValue.strip()}
|
||||
|
||||
proxy_host, proxy_port = parsed['http.proxy'].split(':', 1)
|
||||
proxy_host, proxy_port = config['http.proxy'].split(':', 1)
|
||||
|
||||
cfpath = parsed['http.cookiefile']
|
||||
cfpath = config['http.cookiefile']
|
||||
cj = http.cookiejar.MozillaCookieJar(cfpath)
|
||||
# NOTE: python3.8 doesn't support httponly cookie lines, so we parse
|
||||
# this manually. Once we move to python3.10+, this hack can be removed.
|
||||
@@ -442,8 +450,9 @@ class SSOAuthenticator(_Authenticator):
|
||||
#
|
||||
# 1. writes files to disk.
|
||||
# 2. writes config to stdout, referencing those files.
|
||||
# 3. closes stdout (thus sending EOF to us, allowing
|
||||
# sys.stdout.read() to complete).
|
||||
# 3. closes stdout (sending EOF to us, ending the stdout data).
|
||||
# - Note: on Windows, stdout.read() times out anyway, so
|
||||
# instead we process stdout line by line.
|
||||
# 4. waits for stdin to close.
|
||||
# 5. deletes files on disk (which is why we make sys.stdin a PIPE
|
||||
# instead of closing it outright).
|
||||
@@ -466,24 +475,39 @@ class SSOAuthenticator(_Authenticator):
|
||||
timer = threading.Timer(cls._timeout_secs, _fire_timeout)
|
||||
timer.start()
|
||||
try:
|
||||
stdout_data = proc.stdout.read()
|
||||
config = {}
|
||||
for line in proc.stdout:
|
||||
if not line:
|
||||
break
|
||||
field, value = line.strip().split('=', 1)
|
||||
config[field] = value
|
||||
# Stop reading if we have all the required fields.
|
||||
if cls._required_fields.issubset(config.keys()):
|
||||
break
|
||||
finally:
|
||||
timer.cancel()
|
||||
|
||||
missing_fields = cls._required_fields - config.keys()
|
||||
if timedout:
|
||||
# Within the time limit, at least one required field was
|
||||
# still missing. Killing the process has been triggered,
|
||||
# even if we now have all fields.
|
||||
details = ''
|
||||
if missing_fields:
|
||||
details = ': missing fields [{names}]'.format(
|
||||
names=', '.join(missing_fields))
|
||||
LOGGER.error(
|
||||
'SSOAuthenticator: Timeout: %r: reading config.',
|
||||
cmd)
|
||||
'SSOAuthenticator: Timeout: %r: reading config%s.',
|
||||
cmd, details)
|
||||
raise subprocess.TimeoutExpired(
|
||||
cmd=cmd, timeout=cls._timeout_secs)
|
||||
|
||||
# if the process already ended, then something is wrong.
|
||||
retcode = proc.poll()
|
||||
# if stdout was closed without any data, we need to wait for
|
||||
# end-of-process here and hope for an error message - the
|
||||
# poll above is racy in this case (we could see stdout EOF
|
||||
# but the process may not have quit yet).
|
||||
if not retcode and not stdout_data:
|
||||
# if stdout was closed without all required data, we need to
|
||||
# wait for end-of-process here and hope for an error message
|
||||
# - the poll above is racy in this case (we could see stdout
|
||||
# EOF but the process may not have quit yet).
|
||||
if not retcode and missing_fields:
|
||||
retcode = proc.wait(timeout=cls._timeout_secs)
|
||||
# We timed out while doing `wait` - we can't safely open
|
||||
# stderr on windows, so just emit a generic timeout
|
||||
@@ -505,7 +529,7 @@ class SSOAuthenticator(_Authenticator):
|
||||
f'SSOAuthenticator: exit {retcode}: {stderr.read().strip()}'
|
||||
)
|
||||
|
||||
return cls._parse_config(stdout_data)
|
||||
return cls._parse_config(config)
|
||||
|
||||
@classmethod
|
||||
def _get_sso_info(cls) -> SSOInfo:
|
||||
|
||||
@@ -647,14 +647,14 @@ class SSOAuthenticatorTest(unittest.TestCase):
|
||||
self.assertFalse(self.sso.is_applicable())
|
||||
|
||||
def testParseConfigOK(self):
|
||||
parsed = self.sso._parse_config(
|
||||
textwrap.dedent(f'''
|
||||
somekey=a value with = in it
|
||||
novalue=
|
||||
http.proxy=localhost:12345
|
||||
http.cookiefile={self._input_dir/'cookiefile.txt'}
|
||||
include.path={self._input_dir/'gitconfig'}
|
||||
''').strip())
|
||||
test_config = {
|
||||
'somekey': 'a value with = in it',
|
||||
'novalue': '',
|
||||
'http.proxy': 'localhost:12345',
|
||||
'http.cookiefile': str(self._input_dir / 'cookiefile.txt'),
|
||||
'include.path': str(self._input_dir / 'gitconfig'),
|
||||
}
|
||||
parsed = self.sso._parse_config(test_config)
|
||||
self.assertDictEqual(parsed.headers, {
|
||||
'Authorization': 'Basic REALLY_COOL_TOKEN',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user