Repository: deis/deis

Issue:
Text progress indicator breaks on `formation create` with flavor
```
$ deis create --flavor=ec2-us-west-2
Creating formation... done, created orange-headwind
Git remote deis added

Creating runtime layer... done in 1s
Creating proxy layeTraceback (most recent call last):
  File "/Users/gabriel/bin/deis", line 1514, in <module>
    main()
  File "/Users/gabriel/bin/deis", line 1508, in main
    method(args)
  File "/Users/gabriel/bin/deis", line 771, in formations_create
    self.layers_create({'<id>': 'proxy', '<flavor>': flavor})
  File "/Users/gabriel/bin/deis", line 1072, in layers_create
    progress.start()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 489, in start
    raise RuntimeError("threads can only be started once")
RuntimeError: threads can only be started once
```

We should also have an envvar that allows disabling text progress for the purpose of recording asciicasts.



Discussion:

Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -49,6 +49,7 @@
 from threading import Thread
 import glob
 import json
+import os
 import os.path
 import random
 import re
@@ -218,18 +219,19 @@ def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
 
     def run(self):
         """Write ASCII progress animation frames to stdout."""
-        time.sleep(0.5)
-        self._write_frame(self.frames.next(), erase=False)
-        while not self.cancelled.is_set():
-            time.sleep(0.4)
-            self._write_frame(self.frames.next())
+        if not os.environ.get('DEIS_HIDE_PROGRESS'):
+            time.sleep(0.5)
+            self._write_frame(self.frames.next(), erase=False)
+            while not self.cancelled.is_set():
+                time.sleep(0.4)
+                self._write_frame(self.frames.next())
+            # clear the animation
+            sys.stdout.write('\b' * (len(self.frames.next()) + 2))
+            sys.stdout.flush()
 
     def cancel(self):
         """Set the animation thread as cancelled."""
         self.cancelled.set()
-        # clear the animation
-        sys.stdout.write('\b' * (len(self.frames.next()) + 2))
-        sys.stdout.flush()
 
     def _write_frame(self, frame, erase=True):
         if erase:
@@ -241,9 +243,6 @@ def _write_frame(self, frame, erase=True):
         sys.stdout.flush()
 
 
-progress = TextProgress()
-
-
 def dictify(args):
     """Converts a list of key=val strings into a python dict.
 
@@ -607,6 +606,7 @@ def containers_scale(self, args):
             body.update({typ: int(count)})
         print('Scaling containers... but first, coffee!')
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch('post',
@@ -614,6 +614,7 @@ def containers_scale(self, args):
                                       json.dumps(body))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.ok:  # @UndefinedVariable
             print('done in {}s\n'.format(int(time.time() - before)))
             self.containers_list({})
@@ -748,8 +749,14 @@ def formations_create(self, args):
                 return
         sys.stdout.write('Creating formation... ')
         sys.stdout.flush()
-        response = self._dispatch('post', '/api/formations',
-                                  json.dumps(body))
+        try:
+            progress = TextProgress()
+            progress.start()
+            response = self._dispatch('post', '/api/formations',
+                                      json.dumps(body))
+        finally:
+            progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.created:  # @UndefinedVariable
             data = response.json()
             formation = data['id']
@@ -843,11 +850,13 @@ def formations_destroy(self, args):
         sys.stdout.write("Destroying {}... ".format(formation))
         sys.stdout.flush()
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch('delete', "/api/formations/{}".format(formation))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code in (requests.codes.no_content,  # @UndefinedVariable
                                     requests.codes.not_found):  # @UndefinedVariable
             print('done in {}s'.format(int(time.time() - before)))
@@ -899,12 +908,14 @@ def formations_converge(self, args):
         sys.stdout.write('Converging {}... '.format(formation))
         sys.stdout.flush()
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch('post',
                                       "/api/formations/{}/converge".format(formation))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.ok:  # @UndefinedVariable
             print('done in {}s'.format(int(time.time() - before)))
             databag = json.loads(response.content)
@@ -1069,12 +1080,14 @@ def layers_create(self, args):
         sys.stdout.write("Creating {} layer... ".format(args['<id>']))
         sys.stdout.flush()
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch('post', "/api/formations/{}/layers".format(formation),
                                       json.dumps(body))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.created:  # @UndefinedVariable
             print('done in {}s'.format(int(time.time() - before)))
         else:
@@ -1093,12 +1106,14 @@ def layers_destroy(self, args):
         sys.stdout.write("Destroying {layer} layer... ".format(**locals()))
         sys.stdout.flush()
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch(
                 'delete', "/api/formations/{formation}/layers/{layer}".format(**locals()))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.no_content:  # @UndefinedVariable
             print('done in {}s'.format(int(time.time() - before)))
         else:
@@ -1142,6 +1157,7 @@ def layers_scale(self, args):
             body.update({typ: int(count)})
         print('Scaling layers... but first, coffee!')
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             # TODO: add threaded spinner to print dots
@@ -1150,6 +1166,7 @@ def layers_scale(self, args):
                                       json.dumps(body))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.ok:  # @UndefinedVariable
             print('done in {}s\n'.format(int(time.time() - before)))
             print('Use `git push deis master` to deploy to your formation')
@@ -1241,12 +1258,14 @@ def nodes_destroy(self, args):
         sys.stdout.write("Destroying {}... ".format(node))
         sys.stdout.flush()
         try:
+            progress = TextProgress()
             progress.start()
             before = time.time()
             response = self._dispatch(
                 'delete', "/api/formations/{formation}/nodes/{node}".format(**locals()))
         finally:
             progress.cancel()
+            progress.join()
         if response.status_code == requests.codes.no_content:  # @UndefinedVariable
             print('done in {}s\n'.format(int(time.time() - before)))
         else:


Test patch: