pw_env_setup: Run cipd auth-login if necessary

If the user is not logged in and does not have access to a required path
in CIPD, run 'cipd auth-login' inside bootstrap.

Since the spinner made this awkward, enable pausing the spinner and
pause it while running 'cipd auth-login'.

Tested by running 'cipd auth-logout' followed by bootstrap of a project
with ACL-d packages.

Change-Id: I217c6a97a5161e9a5894a76a35321466ecb672d0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/36900
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Reviewed-by: Joe Ethier <jethier@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
index 84c48c8..c4301ec 100755
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
@@ -61,7 +61,7 @@
     return parser.parse_args(argv)
 
 
-def check_auth(cipd, package_files):
+def check_auth(cipd, package_files, spin):
     """Check have access to CIPD pigweed directory."""
 
     paths = []
@@ -77,12 +77,12 @@
                     parts.pop(-1)
                 paths.append('/'.join(parts))
 
+    username = None
     try:
         output = subprocess.check_output([cipd, 'auth-info'],
                                          stderr=subprocess.STDOUT).decode()
         logged_in = True
 
-        username = None
         match = re.search(r'Logged in as (\S*)\.', output)
         if match:
             username = match.group(1)
@@ -90,38 +90,60 @@
     except subprocess.CalledProcessError:
         logged_in = False
 
-    for path in paths:
-        # Not catching CalledProcessError because 'cipd ls' seems to never
-        # return an error code unless it can't reach the CIPD server.
-        output = subprocess.check_output([cipd, 'ls', path],
-                                         stderr=subprocess.STDOUT).decode()
-        if 'No matching packages' not in output:
-            continue
+    def _check_all_paths():
+        inaccessible_paths = []
 
-        # 'cipd ls' only lists sub-packages but ignores any packages at the
-        # given path. 'cipd instances' will give versions of that package.
-        # 'cipd instances' does use an error code if there's no such package or
-        # that package is inaccessible.
-        try:
-            subprocess.check_output([cipd, 'instances', path],
-                                    stderr=subprocess.STDOUT)
-        except subprocess.CalledProcessError:
+        for path in paths:
+            # Not catching CalledProcessError because 'cipd ls' seems to never
+            # return an error code unless it can't reach the CIPD server.
+            output = subprocess.check_output(
+                [cipd, 'ls', path], stderr=subprocess.STDOUT).decode()
+            if 'No matching packages' not in output:
+                continue
+
+            # 'cipd ls' only lists sub-packages but ignores any packages at the
+            # given path. 'cipd instances' will give versions of that package.
+            # 'cipd instances' does use an error code if there's no such package
+            # or that package is inaccessible.
+            try:
+                subprocess.check_output([cipd, 'instances', path],
+                                        stderr=subprocess.STDOUT)
+            except subprocess.CalledProcessError:
+                inaccessible_paths.append(path)
+
+        return inaccessible_paths
+
+    inaccessible_paths = _check_all_paths()
+
+    if inaccessible_paths and not logged_in:
+        with spin.pause():
             stderr = lambda *args: print(*args, file=sys.stderr)
             stderr()
-            stderr('=' * 60)
-            stderr('ERROR: no access to CIPD path "{}"'.format(path))
-            if logged_in:
-                username_part = ''
-                if username:
-                    username_part = '({}) '.format(username)
-                stderr('Your account {}does not have access to this '
-                       'path'.format(username_part))
-            else:
-                stderr('Try logging in with this command:')
-                stderr()
-                stderr('    {} auth-login'.format(cipd))
-            stderr('=' * 60)
-            return False
+            stderr('No access to the following CIPD paths:')
+            for path in inaccessible_paths:
+                stderr('  {}'.format(path))
+            stderr()
+            stderr('Attempting CIPD login')
+            try:
+                subprocess.check_call([cipd, 'auth-login'])
+            except subprocess.CalledProcessError:
+                stderr('CIPD login failed')
+                return False
+
+        inaccessible_paths = _check_all_paths()
+
+    if inaccessible_paths:
+        stderr = lambda *args: print(*args, file=sys.stderr)
+        stderr('=' * 60)
+        username_part = ''
+        if username:
+            username_part = '({}) '.format(username)
+        stderr('Your account {}does not have access to the following '
+               'paths'.format(username_part))
+        for path in inaccessible_paths:
+            stderr('  {}'.format(path))
+        stderr('=' * 60)
+        return False
 
     return True
 
@@ -151,10 +173,11 @@
     root_install_dir,
     cache_dir,
     env_vars=None,
+    spin=None,
 ):
     """Grab the tools listed in ensure_files."""
 
-    if not check_auth(cipd, package_files):
+    if not check_auth(cipd, package_files, spin):
         return False
 
     # TODO(mohrr) use os.makedirs(..., exist_ok=True).
diff --git a/pw_env_setup/py/pw_env_setup/env_setup.py b/pw_env_setup/py/pw_env_setup/env_setup.py
index 42470c8..b3f1fdd 100755
--- a/pw_env_setup/py/pw_env_setup/env_setup.py
+++ b/pw_env_setup/py/pw_env_setup/env_setup.py
@@ -339,7 +339,7 @@
 
             spin = spinner.Spinner()
             with spin():
-                result = step()
+                result = step(spin)
 
             self._log(result.status_str())
 
@@ -394,7 +394,7 @@
 
         return 0
 
-    def cipd(self):
+    def cipd(self, spin):
         install_dir = os.path.join(self._install_dir, 'cipd')
 
         cipd_client = cipd_wrapper.init(install_dir, silent=True)
@@ -409,12 +409,13 @@
                                   root_install_dir=install_dir,
                                   package_files=package_files,
                                   cache_dir=self._cipd_cache_dir,
-                                  env_vars=self._env):
+                                  env_vars=self._env,
+                                  spin=spin):
             return result(_Result.Status.FAILED)
 
         return result(_Result.Status.DONE)
 
-    def virtualenv(self):
+    def virtualenv(self, unused_spin):
         """Setup virtualenv."""
 
         requirements, req_glob_warnings = _process_globs(
@@ -453,7 +454,7 @@
 
         return result(_Result.Status.DONE)
 
-    def host_tools(self):
+    def host_tools(self, unused_spin):
         # The host tools are grabbed from CIPD, at least initially. If the
         # user has a current host build, that build will be used instead.
         # TODO(mohrr) find a way to do stuff like this for all projects.
@@ -461,14 +462,14 @@
         self._env.prepend('PATH', os.path.join(host_dir, 'host_tools'))
         return _Result(_Result.Status.DONE)
 
-    def win_scripts(self):
+    def win_scripts(self, unused_spin):
         # These scripts act as a compatibility layer for windows.
         env_setup_dir = os.path.join(self._pw_root, 'pw_env_setup')
         self._env.prepend('PATH', os.path.join(env_setup_dir,
                                                'windows_scripts'))
         return _Result(_Result.Status.DONE)
 
-    def cargo(self):
+    def cargo(self, unused_spin):
         install_dir = os.path.join(self._install_dir, 'cargo')
 
         package_files, glob_warnings = _process_globs(self._cargo_package_file)
diff --git a/pw_env_setup/py/pw_env_setup/spinner.py b/pw_env_setup/py/pw_env_setup/spinner.py
index 44990be..63d577ea 100644
--- a/pw_env_setup/py/pw_env_setup/spinner.py
+++ b/pw_env_setup/py/pw_env_setup/spinner.py
@@ -65,3 +65,11 @@
             yield self
         finally:
             self.stop()
+
+    @contextlib.contextmanager
+    def pause(self):
+        try:
+            self.stop()
+            yield self
+        finally:
+            self.start()