Merge "Clarify uniqueness of some config items"
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index d3a6ec8..4af5b47 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -2929,9 +2929,9 @@
         ])
 
     def test_nodeset_branch_duplicate(self):
-        # Test that we can create a duplicate secret on a different
+        # Test that we can create a duplicate nodeset on a different
         # branch of the same project -- i.e., that when we branch
-        # master to stable on a project with a secret, nothing
+        # master to stable on a project with a nodeset, nothing
         # changes.
         self.create_branch('org/project1', 'stable')
         self.fake_gerrit.addEvent(
diff --git a/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index bf11c6f..b299219 100755
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -171,14 +171,6 @@
         return pid_fn
 
     @abc.abstractmethod
-    def exit_handler(self, signum, frame):
-        """
-        This is a signal handler which is called on SIGINT and SIGTERM and must
-        take care of stopping the application.
-        """
-        pass
-
-    @abc.abstractmethod
     def run(self):
         """
         This is the main run method of the application.
@@ -197,8 +189,6 @@
         signal.signal(signal.SIGUSR2, stack_dump_handler)
 
         if self.args.nodaemon:
-            signal.signal(signal.SIGTERM, self.exit_handler)
-            signal.signal(signal.SIGINT, self.exit_handler)
             self.run()
         else:
             # Exercise the pidfile before we do anything else (including
diff --git a/zuul/cmd/executor.py b/zuul/cmd/executor.py
index 5b06f0c..b050a59 100755
--- a/zuul/cmd/executor.py
+++ b/zuul/cmd/executor.py
@@ -17,6 +17,7 @@
 import logging
 import os
 import sys
+import signal
 import tempfile
 
 import zuul.cmd
@@ -50,6 +51,8 @@
 
     def exit_handler(self, signum, frame):
         self.executor.stop()
+        self.executor.join()
+        sys.exit(0)
 
     def start_log_streamer(self):
         pipe_read, pipe_write = os.pipe()
@@ -106,7 +109,16 @@
                                        log_streaming_port=self.finger_port)
         self.executor.start()
 
-        self.executor.join()
+        if self.args.nodaemon:
+            signal.signal(signal.SIGTERM, self.exit_handler)
+            while True:
+                try:
+                    signal.pause()
+                except KeyboardInterrupt:
+                    print("Ctrl + C: asking executor to exit nicely...\n")
+                    self.exit_handler(signal.SIGINT, None)
+        else:
+            self.executor.join()
 
 
 def main():
diff --git a/zuul/cmd/fingergw.py b/zuul/cmd/fingergw.py
index 0d47f08..920eed8 100644
--- a/zuul/cmd/fingergw.py
+++ b/zuul/cmd/fingergw.py
@@ -14,6 +14,7 @@
 # under the License.
 
 import logging
+import signal
 import sys
 
 import zuul.cmd
@@ -46,9 +47,6 @@
         if self.args.command:
             self.args.nodaemon = True
 
-    def exit_handler(self, signum, frame):
-        self.stop()
-
     def run(self):
         '''
         Main entry point for the FingerGatewayApp.
@@ -86,7 +84,19 @@
         self.log.info('Starting Zuul finger gateway app')
         self.gateway.start()
 
-        self.gateway.wait()
+        if self.args.nodaemon:
+            # NOTE(Shrews): When running in non-daemon mode, although sending
+            # the 'stop' command via the command socket will shutdown the
+            # gateway, it's still necessary to Ctrl+C to stop the app.
+            while True:
+                try:
+                    signal.pause()
+                except KeyboardInterrupt:
+                    print("Ctrl + C: asking gateway to exit nicely...\n")
+                    self.stop()
+                    break
+        else:
+            self.gateway.wait()
 
         self.log.info('Stopped Zuul finger gateway app')
 
diff --git a/zuul/cmd/merger.py b/zuul/cmd/merger.py
index 390191f..8c47989 100755
--- a/zuul/cmd/merger.py
+++ b/zuul/cmd/merger.py
@@ -14,6 +14,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import signal
 import sys
 
 import zuul.cmd
@@ -43,6 +44,8 @@
 
     def exit_handler(self, signum, frame):
         self.merger.stop()
+        self.merger.join()
+        sys.exit(0)
 
     def run(self):
         # See comment at top of file about zuul imports
@@ -59,7 +62,16 @@
                                                      self.connections)
         self.merger.start()
 
-        self.merger.join()
+        if self.args.nodaemon:
+            signal.signal(signal.SIGTERM, self.exit_handler)
+            while True:
+                try:
+                    signal.pause()
+                except KeyboardInterrupt:
+                    print("Ctrl + C: asking merger to exit nicely...\n")
+                    self.exit_handler(signal.SIGINT, None)
+        else:
+            self.merger.join()
 
 
 def main():
diff --git a/zuul/cmd/scheduler.py b/zuul/cmd/scheduler.py
index 3cffa10..7748a80 100755
--- a/zuul/cmd/scheduler.py
+++ b/zuul/cmd/scheduler.py
@@ -63,7 +63,9 @@
 
     def exit_handler(self, signum, frame):
         self.sched.exit()
+        self.sched.join()
         self.stop_gear_server()
+        sys.exit(0)
 
     def start_gear_server(self):
         pipe_read, pipe_write = os.pipe()
@@ -173,7 +175,16 @@
 
         signal.signal(signal.SIGHUP, self.reconfigure_handler)
 
-        self.sched.join()
+        if self.args.nodaemon:
+            signal.signal(signal.SIGTERM, self.exit_handler)
+            while True:
+                try:
+                    signal.pause()
+                except KeyboardInterrupt:
+                    print("Ctrl + C: asking scheduler to exit nicely...\n")
+                    self.exit_handler(signal.SIGINT, None)
+        else:
+            self.sched.join()
 
 
 def main():
diff --git a/zuul/cmd/web.py b/zuul/cmd/web.py
index 78392db..ad3062f 100755
--- a/zuul/cmd/web.py
+++ b/zuul/cmd/web.py
@@ -89,6 +89,12 @@
                                        name='web')
         self.thread.start()
 
+        try:
+            signal.pause()
+        except KeyboardInterrupt:
+            print("Ctrl + C: asking web server to exit nicely...\n")
+            self.exit_handler(signal.SIGINT, None)
+
         self.thread.join()
         loop.stop()
         loop.close()
diff --git a/zuul/lib/connections.py b/zuul/lib/connections.py
index 33c66f9..3b3f1ae 100644
--- a/zuul/lib/connections.py
+++ b/zuul/lib/connections.py
@@ -170,6 +170,16 @@
         connection = self.connections[connection_name]
         return connection.driver.getTrigger(connection, config)
 
+    def getSourceByHostname(self, hostname):
+        for connection in self.connections.values():
+            if hasattr(connection, 'canonical_hostname'):
+                if connection.canonical_hostname == hostname:
+                    return self.getSource(connection.connection_name)
+            if hasattr(connection, 'server'):
+                if connection.server == hostname:
+                    return self.getSource(connection.connection_name)
+        return None
+
     def getSourceByCanonicalHostname(self, canonical_hostname):
         for connection in self.connections.values():
             if hasattr(connection, 'canonical_hostname'):
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index b8a280f..88ddf7d 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -360,7 +360,7 @@
                 url = urllib.parse.urlparse(match)
             except ValueError:
                 continue
-            source = self.sched.connections.getSourceByCanonicalHostname(
+            source = self.sched.connections.getSourceByHostname(
                 url.hostname)
             if not source:
                 continue
@@ -722,9 +722,10 @@
         return True
 
     def onBuildCompleted(self, build):
-        self.log.debug("Build %s completed" % build)
         item = build.build_set.item
 
+        self.log.debug("Build %s of %s completed" % (build, item.change))
+
         item.setResult(build)
         item.pipeline.layout.tenant.semaphore_handler.release(item, build.job)
         self.log.debug("Item %s status is now:\n %s" %