Ctrl-C Improvements (Anthony Roach)
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 18 May 2002 05:36:40 +0000 (05:36 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 18 May 2002 05:36:40 +0000 (05:36 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@377 fdb21ef1-2011-0410-befe-b5e4ea1792b1

etc/TestCmd.py
src/CHANGES.txt
src/engine/SCons/Job.py
src/engine/SCons/JobTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Script/__init__.py

index 06e324f7c7259ad28ae1e4c5f16413cf993b18b8..17d520bd0e31c898abffad0f7ff08567475d59f7 100644 (file)
@@ -510,7 +510,10 @@ class TestCmd:
        elif run < 0:
            run = len(self._stderr) + run
        run = run - 1
-       return self._stderr[run]
+       if run >= len(self._stderr) or run < 0:
+           return ''
+       else:
+           return self._stderr[run]
 
     def stdout(self, run = None):
        """Returns the standard output from the specified run number.
@@ -524,7 +527,10 @@ class TestCmd:
        elif run < 0:
            run = len(self._stdout) + run
        run = run - 1
-       return self._stdout[run]
+       if run >= len(self._stdout) or run < 0:
+           return ''
+       else:
+           return self._stdout[run]
 
     def subdir(self, *subdirs):
        """Create new subdirectories under the temporary working
index 62749910d1a50a9bc113c57024eb9feb75cc282d..beb47d7718c626488a2dff0c223cc2bfd2c2980c 100644 (file)
@@ -46,6 +46,9 @@ RELEASE 0.08 -
   - Fall back to importing the SCons.TimeStamp module if the SCons.MD5
     module can't be imported.
 
+  - Fix interrupt handling to guarantee that a single interrupt will
+    halt SCons both when using -j and not.
+
   From Zed Shaw:
 
   - Add an Append() method to Environments, to append values to
index 1eb5fa084be627e134e80aa8e267639d7934076d..21f4617a4b180f1f0c478da64e5cb4be9442b5e2 100644 (file)
@@ -31,6 +31,8 @@ stop, and wait on jobs.
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import time
+
 class Jobs:
     """An instance of this class initializes N jobs, and provides
     methods for starting, stopping, and waiting on all N jobs.
@@ -44,34 +46,47 @@ class Jobs:
         otherwise 'num' parallel jobs will be used.
         """
 
+        # Keeps track of keyboard interrupts:
+        self.keyboard_interrupt = 0
+
         if num > 1:
             self.jobs = []
             for i in range(num):
                 self.jobs.append(Parallel(taskmaster, self))
         else:
-            self.jobs = [Serial(taskmaster)]
-
-    def start(self):
-        """start the jobs"""
+            self.jobs = [Serial(taskmaster, self)]
+   
+        self.running = []
 
-        for job in self.jobs:
-            job.start()
+    def run(self):
+        """run the jobs, and wait for them to finish"""
 
-    def wait(self):
-        """ wait for the jobs started with start() to finish"""
-
-        for job in self.jobs:
-            job.wait()
-
-    def stop(self):
-        """
-        stop the jobs started with start()
+        try:
+            for job in self.jobs:
+                job.start()
+                self.running.append(job)
+            while self.running:
+                self.running[-1].wait()
+                self.running.pop()
+        except KeyboardInterrupt:
+            # mask any further keyboard interrupts so that scons
+            # can shutdown cleanly:
+            # (this only masks the keyboard interrupt for Python,
+            #  child processes can still get the keyboard interrupt)
+            import signal
+            signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+            for job in self.running:
+                job.keyboard_interrupt()
+            else:
+                self.keyboard_interrupt = 1
 
-        This function does not wait for the jobs to finish.
-        """
+            # wait on any remaining jobs to finish:
+            for job in self.running:
+                job.wait()
 
-        for job in self.jobs:
-            job.stop()
+        if self.keyboard_interrupt:
+            raise KeyboardInterrupt
     
 class Serial:
     """This class is used to execute tasks in series, and is more efficient
@@ -81,7 +96,7 @@ class Serial:
     This class is not thread safe.
     """
 
-    def __init__(self, taskmaster):
+    def __init__(self, taskmaster, jobs):
         """Create a new serial job given a taskmaster. 
 
         The taskmaster's next_task() method should return the next task
@@ -92,6 +107,7 @@ class Serial:
         is_blocked() method will not be called.  """
         
         self.taskmaster = taskmaster
+        self.jobs = jobs
 
     def start(self):
         
@@ -100,7 +116,7 @@ class Serial:
         fails to execute (i.e. execute() raises an exception), then the job will
         stop."""
         
-        while 1:
+        while not self.jobs.keyboard_interrupt:
             task = self.taskmaster.next_task()
 
             if task is None:
@@ -108,6 +124,8 @@ class Serial:
 
             try:
                 task.execute()
+            except KeyboardInterrupt:
+                raise
             except:
                 # Let the failed() callback function arrange for the
                 # build to stop if that's appropriate.
@@ -115,16 +133,13 @@ class Serial:
             else:
                 task.executed()
 
-    def stop(self):
-        """Serial jobs are always finished when start() returns, so there
-        is nothing to do here"""
-        
-        pass
-
     def wait(self):
         """Serial jobs are always finished when start() returns, so there
         is nothing to do here"""
         pass
+    
+    def keyboard_interrupt(self):
+        self.jobs.keyboard_interrupt = 1
 
 
 # The will hold a condition variable once the first parallel task
@@ -171,7 +186,6 @@ class Parallel:
         self.taskmaster = taskmaster
         self.jobs = jobs
         self.thread = threading.Thread(None, self.__run)
-        self.stop_running = 0
 
         if cv is None:
             cv = threading.Condition()
@@ -181,38 +195,33 @@ class Parallel:
         tasks from the task master and executing them. This method returns
         immediately and doesn't wait for the jobs to be executed.
 
-        If a task fails to execute (i.e. execute() raises an exception),
-        all jobs will be stopped.
-
-        To stop the job, call stop().
         To wait for the job to finish, call wait().
         """
         self.thread.start()
 
-    def stop(self):
-        """Stop the job. This will cause the job to finish after the
-        currently executing task is done. A job that has been stopped can
-        not be restarted.
+    def wait(self):
+        """Wait for the job to finish. A job is finished when there
+        are no more tasks.
 
-        To wait for the job to finish, call wait().
+        This method should only be called after start() has been called.
         """
 
+        # Sleeping in a loop like this is lame. Calling 
+        # self.thread.join() would be much nicer, but
+        # on Linux self.thread.join() doesn't always
+        # return when a KeyboardInterrupt happens, and when
+        # it does return, it causes Python to hang on shutdown.
+        # In other words this is just
+        # a work-around for some bugs/limitations in the
+        # self.thread.join() method.
+        while self.thread.isAlive():
+            time.sleep(0.5)
+
+    def keyboard_interrupt(self):
         cv.acquire()
-        self.stop_running = 1
-        # wake up the sleeping jobs so this job will end as soon as possible:
-        cv.notifyAll() 
+        self.jobs.keyboard_interrupt = 1
+        cv.notifyAll()
         cv.release()
-        
-    def wait(self):
-        """Wait for the job to finish. A job is finished when either there
-        are no more tasks or the job has been stopped and it is no longer
-        executing a task.
-
-        This method should only be called after start() has been called.
-
-        To stop the job, call stop().
-        """
-        self.thread.join()
 
     def __run(self):
         """private method that actually executes the tasks"""
@@ -222,13 +231,11 @@ class Parallel:
         try:
 
             while 1:
-                while self.taskmaster.is_blocked() and not self.stop_running:
+                while (self.taskmaster.is_blocked() and 
+                       not self.jobs.keyboard_interrupt):
                     cv.wait(None)
 
-                # check this before calling next_task(), because
-                # this job may have been stopped because of a build
-                # failure:
-                if self.stop_running:
+                if self.jobs.keyboard_interrupt:
                     break
                     
                 task = self.taskmaster.next_task()
@@ -242,6 +249,8 @@ class Parallel:
                         task.execute()
                     finally:
                         cv.acquire()
+                except KeyboardInterrupt:
+                    self.jobs.keyboard_interrupt = 1
                 except:
                     # Let the failed() callback function arrange for
                     # calling self.jobs.stop() to to stop the build
@@ -253,7 +262,8 @@ class Parallel:
                 # signal the cv whether the task failed or not,
                 # or otherwise the other Jobs might
                 # remain blocked:
-                if not self.taskmaster.is_blocked():
+                if (not self.taskmaster.is_blocked() or
+                    self.jobs.keyboard_interrupt):
                     cv.notifyAll()
                     
         finally:
index f7c180c7a82d4de0c83f58b357d1d706d2359d31..55e5f0f5a09b1aeb9856d14e9ed6e65b452445f0 100644 (file)
@@ -172,8 +172,7 @@ class ParallelTestCase(unittest.TestCase):
 
         taskmaster = Taskmaster(num_tasks, self, Task)
         jobs = SCons.Job.Jobs(num_jobs, taskmaster)
-        jobs.start()
-        jobs.wait()
+        jobs.run()
 
         self.failUnless(not taskmaster.tasks_were_serial(),
                         "the tasks were not executed in parallel")
@@ -190,8 +189,7 @@ class SerialTestCase(unittest.TestCase):
 
         taskmaster = Taskmaster(num_tasks, self, Task)
         jobs = SCons.Job.Jobs(1, taskmaster)
-        jobs.start()
-        jobs.wait()
+        jobs.run()
 
         self.failUnless(taskmaster.tasks_were_serial(),
                         "the tasks were not executed in series")
@@ -208,8 +206,7 @@ class SerialExceptionTestCase(unittest.TestCase):
 
         taskmaster = Taskmaster(num_tasks, self, ExceptionTask)
         jobs = SCons.Job.Jobs(1, taskmaster)
-        jobs.start()
-        jobs.wait()
+        jobs.run()
 
         self.failIf(taskmaster.num_executed,
                     "a task was executed")
@@ -224,8 +221,7 @@ class ParallelExceptionTestCase(unittest.TestCase):
 
         taskmaster = Taskmaster(num_tasks, self, ExceptionTask)
         jobs = SCons.Job.Jobs(num_jobs, taskmaster)
-        jobs.start()
-        jobs.wait()
+        jobs.run()
 
         self.failIf(taskmaster.num_executed,
                     "a task was executed")
index fac20e3a77f6f3dd49d2a6888a42d5d5816b20fb..b67d35207c072fa13e67d661f9d01dd9251a12c8 100644 (file)
@@ -570,7 +570,7 @@ class NodeTestCase(unittest.TestCase):
                 assert nodes[0].name == u"Util.py"
                 assert nodes[1].name == u"UtilTests.py"
                 \n"""
-            exec code
+            exec code in globals(), locals()
 
         nodes = SCons.Node.arg2nodes(["Util.py", "UtilTests.py"], Factory)
         assert len(nodes) == 2, nodes
index 472ec074f82dfc63a593d58f4e26c910308fbc36..e44006046190fcea598f2737d5eacedd614a3fe2 100644 (file)
@@ -114,6 +114,8 @@ class Node:
             try:
                 stat = apply(self.builder.execute, (),
                              self.generate_build_args())
+            except KeyboardInterrupt:
+                raise
             except:
                 raise BuildError(self, "Exception",
                                  sys.exc_type,
index d0b17d446ab151fdf90e5ca0f9d8df8cb157d647..2fbde13b905a421de666da4bd38d51694ed2cd91 100644 (file)
@@ -870,8 +870,7 @@ def _main():
     jobs = SCons.Job.Jobs(num_jobs, taskmaster)
 
     try:
-        jobs.start()
-        jobs.wait()
+        jobs.run()
     finally:
         SCons.Sig.write()
 
@@ -882,7 +881,7 @@ def main():
         pass
     except KeyboardInterrupt:
         print "Build interrupted."
-        sys.exit(1)
+        sys.exit(2)
     except SyntaxError, e:
         _scons_syntax_error(e)
     except UserError, e: