395148: avoid certain hangs in shutdown and document the different shutdown cases
diff --git a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/CoreBundleActivator.java b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/CoreBundleActivator.java
index 715875a..b20dff6 100644
--- a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/CoreBundleActivator.java
+++ b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/CoreBundleActivator.java
@@ -211,8 +211,8 @@
 
     protected Shutdown createShutdown(BundleContext context, EventLogger eventLogger) {
         Framework framework = (Framework) context.getBundle(0);
-
-        ShutdownManager manager = new ShutdownManager(eventLogger, framework);
+        Runtime runtime = Runtime.getRuntime();
+        ShutdownManager manager = new ShutdownManager(eventLogger, framework, runtime);
         return manager;
     }
 
diff --git a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/ShutdownManager.java b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/ShutdownManager.java
index de21247..d49024e 100644
--- a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/ShutdownManager.java
+++ b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/core/internal/ShutdownManager.java
@@ -14,6 +14,9 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.eclipse.virgo.medic.eventlog.EventLogger;
+import org.eclipse.virgo.nano.core.Shutdown;
+import org.eclipse.virgo.nano.diagnostics.KernelLogEvents;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleEvent;
 import org.osgi.framework.BundleException;
@@ -23,18 +26,47 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-
-import org.eclipse.virgo.nano.core.Shutdown;
-import org.eclipse.virgo.nano.diagnostics.KernelLogEvents;
-import org.eclipse.virgo.medic.eventlog.EventLogger;
-
 /**
- * Standard implementation of {@link Shutdown} that performs all shutdown actions synchronously.
+ * Standard implementation of {@link Shutdown} that may be called to initiate JVM shutdown. This class also listens for
+ * <i>unsolicited</i> shutdown (that is, shutdown not initiated by this class) of the JVM or the OSGi framework and
+ * reacts accordingly.
  * <p />
- * This implementation registers a {@link Runtime#addShutdownHook(Thread) shutdown hook} to perform graceful shutdown
- * when the user kills the VM.
+ * JVM shutdown may be initiated in either of two ways:
+ * <ol>
+ * <li>By this class (when a program calls one of the methods of the {@link Shutdown} interface). If graceful shutdown
+ * is requested, this class shuts down the OSGi framework and then the JVM. If immediate shutdown is requested, this
+ * class shuts down the JVM.</li>
+ * <li>Not by this class. A {@link Runtime#addShutdownHook(Thread) shutdown hook} previously registered by this class
+ * responds to JVM shutdown by shutting down the OSGi framework before allowing JVM shutdown to continue. Note that
+ * {@link System#exit} must not be called under the shutdown hook as this can block indefinitely (see the javadoc of
+ * {@link Runtime#exit}).</li>
+ * </ol>
+ * If this class attempts to shut down the OSGi framework but this takes longer than {@code SHUTDOWN_TIMOUT}, an error
+ * message is written to the event log and the JVM is halted abruptly with a non-zero exit code.
  * <p />
- * 
+ * When OSGi framework shutdown occurs, a synchronous bundle listener previously registered by this class unregisters
+ * the shutdown hook (unless this class initiated the OSGi framework shutdown in response to an unsolicited JVM
+ * shutdown, in which case the shutdown hook is left in place).</li>
+ * <p />
+ * This class expects some recursive invocations and avoids others:
+ * <ul>
+ * <li>A graceful shutdown request on the {@link Shutdown} interface normally results in the synchronous bundle listener
+ * being driven on OSGi framework shutdown.</li>
+ * <li>An immediate shutdown request on the {@link Shutdown} interface unregisters the shutdown hook so that it is not
+ * driven when the JVM is shut down.</li>
+ * <li>An unsolicited JVM termination prevents the shutdown hook from being unregistered.</li>
+ * </ul>
+ * <p />
+ * So, in summary, the JVM may terminate in the following ways:
+ * <ol>
+ * <li>Solicited graceful shutdown, which attempts to stop the OSGi framework and, if successful, exits the JVM.</li>
+ * <li>Unsolicited graceful shutdown, which attempts to stop the OSGi framework and, if successful, allows JVM
+ * termination to continue.</li>
+ * <li>Solicited immediate shutdown, which exits the JVM.</li>
+ * <li>Solicited halt if an attempt by this class to stop the OSGi framework fails or times out.</li>
+ * <li>Unsolicited halt, which does not involve this class.</li>
+ * </ol>
+ * <p />
  * <strong>Concurrent Semantics</strong><br />
  * 
  * Threadsafe
@@ -42,6 +74,10 @@
  */
 class ShutdownManager implements Shutdown {
 
+    private static final int NORMAL_TERMINATION_EXIT_CODE = 0;
+
+    private static final int GRACEFUL_TERMINATION_FAILURE_EXIT_CODE = 1;
+
     private static final Logger LOGGER = LoggerFactory.getLogger(ShutdownManager.class);
 
     private static final long SHUTDOWN_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
@@ -56,13 +92,17 @@
 
     private final Framework framework;
 
+    private final Runtime runtime;
+
     private final Thread shutdownHook = new Thread(new Runnable() {
 
+        @Override
         public void run() {
             try {
-                // set to stopping so we don't try to remove the shutdown hook later
-                state.set(STATE_STOPPING);
-                ShutdownManager.this.shutdown();
+                // set to stopping so we don't remove the shutdown hook later
+                if (ShutdownManager.this.compareAndSetHookStopping()) {
+                    ShutdownManager.this.doShutdown(false);
+                }
             } catch (Throwable t) {
                 t.printStackTrace();
             }
@@ -70,10 +110,11 @@
 
     });
 
-    public ShutdownManager(EventLogger eventLogger, Framework framework) {
+    public ShutdownManager(EventLogger eventLogger, Framework framework, Runtime runtime) {
         this.eventLogger = eventLogger;
         this.framework = framework;
-        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
+        this.runtime = runtime;
+        runtime.addShutdownHook(this.shutdownHook);
         BundleContext bundleContext = framework.getBundleContext();
         bundleContext.addBundleListener(new ShutdownLoggingListener());
     }
@@ -81,7 +122,12 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public void shutdown() {
+        doShutdown(true);
+    }
+
+    private void doShutdown(boolean solicitedShutdown) {
         FrameworkEvent shutdownResponse = null;
         try {
             this.framework.stop();
@@ -92,38 +138,66 @@
             LOGGER.error("Interrupted during shutdown.", ex);
         }
 
-        if (!isSuccessfulStopResponse(shutdownResponse)) {
-            immediateShutdown();
+        if (isSuccessfulStopResponse(shutdownResponse)) {
+            // If this class initiated shutdown, shut down the JVM. Otherwise allow JVM termination to continue.
+            if (solicitedShutdown) {
+                initiateJvmTermination();
+            }
         } else {
-        	removeShutdownHook();
+            // Escalate to JVM halt.
+            this.eventLogger.log(KernelLogEvents.SHUTDOWN_HALTED);
+            haltJvm(GRACEFUL_TERMINATION_FAILURE_EXIT_CODE);
         }
     }
 
+    private void initiateJvmTermination() {
+        removeShutdownHook();
+        exitJvm();
+    }
+
     /**
      * {@inheritDoc}
      */
+    @Override
     public void immediateShutdown() {
-        removeShutdownHook();
         this.eventLogger.log(KernelLogEvents.IMMEDIATE_SHUTDOWN_INITIATED);
-        exitVM();
+        initiateJvmTermination();
     }
-    
-    protected void exitVM() {
-    	System.exit(0);
+
+    /**
+     * This method must not be overridden except by testcases.
+     */
+    protected void exitJvm() {
+        System.exit(NORMAL_TERMINATION_EXIT_CODE);
+    }
+
+    /**
+     * This method must not be overridden except by testcases.
+     */
+    protected void haltJvm(int status) {
+        this.runtime.halt(status);
     }
 
     private boolean isSuccessfulStopResponse(FrameworkEvent shutdownResponse) {
         return shutdownResponse != null && shutdownResponse.getType() == FrameworkEvent.STOPPED;
     }
 
-    private void removeShutdownHook() {
-        if(this.state.compareAndSet(STATE_RUNNING, STATE_STOPPING)) {
-            Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
+    /**
+     * This method must only be called by testcases.
+     */
+    final void removeShutdownHook() {
+        if (compareAndSetHookStopping()) {
+            this.runtime.removeShutdownHook(this.shutdownHook);
         }
     }
 
+    private boolean compareAndSetHookStopping() {
+        return this.state.compareAndSet(STATE_RUNNING, STATE_STOPPING);
+    }
+
     private final class ShutdownLoggingListener implements SynchronousBundleListener {
 
+        @Override
         public void bundleChanged(BundleEvent event) {
             BundleContext bundleContext = ShutdownManager.this.framework.getBundleContext();
             if (BundleEvent.STOPPING == event.getType() && event.getBundle() == bundleContext.getBundle()) {
diff --git a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/diagnostics/KernelLogEvents.java b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/diagnostics/KernelLogEvents.java
index 8e9507c..843c1e6 100644
--- a/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/diagnostics/KernelLogEvents.java
+++ b/org.eclipse.virgo.nano.core/src/main/java/org/eclipse/virgo/nano/diagnostics/KernelLogEvents.java
@@ -35,6 +35,7 @@
     
     SHUTDOWN_INITIATED(10, Level.INFO), //
     IMMEDIATE_SHUTDOWN_INITIATED(11, Level.INFO), //
+    SHUTDOWN_HALTED(12, Level.ERROR), //
     
     APPLICATION_CONTEXT_DEPENDENCY_DELAYED(100, Level.WARNING), //
     APPLICATION_CONTEXT_DEPENDENCY_SATISFIED(101, Level.INFO), //
diff --git a/org.eclipse.virgo.nano.core/src/main/resources/EventLogMessages.properties b/org.eclipse.virgo.nano.core/src/main/resources/EventLogMessages.properties
index 1cb7745..1405dd9 100644
--- a/org.eclipse.virgo.nano.core/src/main/resources/EventLogMessages.properties
+++ b/org.eclipse.virgo.nano.core/src/main/resources/EventLogMessages.properties
@@ -10,6 +10,7 @@
 
 KE0010I = Shutdown initiated.
 KE0011I = Immediate shutdown initiated.
+KE0012E = Shutdown failed - halting JVM.
 
 KE0100W = Reference '{}' in bundle '{}' version '{}' is waiting for service with filter '{}'.
 KE0101I = Reference '{}' in bundle '{}' version '{}' was satisfied by service with filter '{}'.
diff --git a/org.eclipse.virgo.nano.core/src/test/java/org/eclipse/virgo/nano/core/internal/ShutdownManagerTests.java b/org.eclipse.virgo.nano.core/src/test/java/org/eclipse/virgo/nano/core/internal/ShutdownManagerTests.java
index 4df2f92..230a871 100644
--- a/org.eclipse.virgo.nano.core/src/test/java/org/eclipse/virgo/nano/core/internal/ShutdownManagerTests.java
+++ b/org.eclipse.virgo.nano.core/src/test/java/org/eclipse/virgo/nano/core/internal/ShutdownManagerTests.java
@@ -16,6 +16,7 @@
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.util.List;
@@ -34,70 +35,101 @@
 import org.eclipse.virgo.test.stubs.framework.StubBundleContext;
 
 public class ShutdownManagerTests {
-	
-	private final Framework framework = createMock(Framework.class);
-	
-	private final MockEventLogger eventLogger = new MockEventLogger();
-	
-	private final StubBundle bundle = new StubBundle();
-	
-	private final StubBundleContext bundleContext = new StubBundleContext(bundle);
-	
-	@Test
-	public void shutdown() throws BundleException, InterruptedException {
 
-		expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
-		this.framework.stop();
-		expect(this.framework.waitForStop(30000)).andReturn(new FrameworkEvent(FrameworkEvent.STOPPED, this.bundle, null));
-		
-		replay(this.framework);
-		
-		ShutdownManager shutdownManager = new ShutdownManager(this.eventLogger, this.framework);		
-		shutdownManager.shutdown();
-		
-		verify(this.framework);
-	}
-	
-	@Test
-	public void failedShutdownDrivesImmediateShutdown() throws Exception {
-		expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
-		this.framework.stop();
-		expect(this.framework.waitForStop(30000)).andReturn(new FrameworkEvent(FrameworkEvent.WAIT_TIMEDOUT, this.bundle, null));
-		
-		replay(this.framework);
-		
-		UnitTestShutdownManager shutdownManager = new UnitTestShutdownManager(this.eventLogger, this.framework);		
-		shutdownManager.shutdown();
-		
-		verify(this.framework);
-		
-		this.eventLogger.isLogged("KE0011I");
-	}
-	
-	@Test
-	public void shutdownMessageLogged() {
-		expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
-		
-		replay(this.framework);		
-		ShutdownManager shutdownManager = new ShutdownManager(this.eventLogger, this.framework);
-		verify(this.framework);
-		
-		List<BundleListener> bundleListeners = this.bundleContext.getBundleListeners();
-		
-		assertEquals(1, bundleListeners.size());		
-		bundleListeners.get(0).bundleChanged(new BundleEvent(BundleEvent.STOPPING, this.bundle));
-		
-		assertTrue(this.eventLogger.isLogged("KE0010I"));		
-	}
-	
-	private static final class UnitTestShutdownManager extends ShutdownManager {				
+    private final Framework framework = createMock(Framework.class);
+    
+    private final Runtime runtime = Runtime.getRuntime();
 
-		public UnitTestShutdownManager(EventLogger eventLogger, Framework framework) {
-			super(eventLogger, framework);
-		}
+    private final MockEventLogger eventLogger = new MockEventLogger();
 
-		@Override
-		public void exitVM() {
-		}						
-	}
+    private final StubBundle bundle = new StubBundle();
+
+    private final StubBundleContext bundleContext = new StubBundleContext(bundle);
+
+    @Test
+    public void shutdown() throws BundleException, InterruptedException {
+
+        expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
+        this.framework.stop();
+        expect(this.framework.waitForStop(30000)).andReturn(new FrameworkEvent(FrameworkEvent.STOPPED, this.bundle, null));
+
+        replay(this.framework);
+
+        UnitTestShutdownManager shutdownManager = new UnitTestShutdownManager(this.eventLogger, this.framework, this.runtime);
+        shutdownManager.shutdown();
+
+        verify(this.framework);
+        
+        assertTrue(shutdownManager.isExited());
+        assertFalse(shutdownManager.isHalted());
+    }
+
+    @Test
+    public void failedShutdownDrivesImmediateShutdown() throws Exception {
+        expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
+        this.framework.stop();
+        expect(this.framework.waitForStop(30000)).andReturn(new FrameworkEvent(FrameworkEvent.WAIT_TIMEDOUT, this.bundle, null));
+
+        replay(this.framework);
+
+        UnitTestShutdownManager shutdownManager = new UnitTestShutdownManager(this.eventLogger, this.framework, this.runtime);
+        shutdownManager.shutdown();
+
+        verify(this.framework);
+
+        assertTrue(this.eventLogger.isLogged("KE0012E"));
+        assertFalse(shutdownManager.isExited());
+        assertTrue(shutdownManager.isHalted());
+        
+        shutdownManager.removeShutdownHook(); // avoid error messages on the console
+    }
+
+    @Test
+    public void shutdownMessageLogged() {
+        expect(this.framework.getBundleContext()).andReturn(this.bundleContext).anyTimes();
+
+        replay(this.framework);
+        UnitTestShutdownManager shutdownManager = new UnitTestShutdownManager(this.eventLogger, this.framework, this.runtime);
+        verify(this.framework);
+
+        List<BundleListener> bundleListeners = this.bundleContext.getBundleListeners();
+
+        assertEquals(1, bundleListeners.size());
+        bundleListeners.get(0).bundleChanged(new BundleEvent(BundleEvent.STOPPING, this.bundle));
+
+        assertTrue(this.eventLogger.isLogged("KE0010I"));
+        assertFalse(shutdownManager.isExited());
+        assertFalse(shutdownManager.isHalted());
+
+    }
+
+    private static final class UnitTestShutdownManager extends ShutdownManager {
+        
+        private volatile boolean exited = false;
+        
+        private volatile boolean halted = false;
+
+        public UnitTestShutdownManager(EventLogger eventLogger, Framework framework, Runtime runtime) {
+            super(eventLogger, framework, runtime);
+        }
+
+        @Override
+        protected void exitJvm() {
+            this.exited = true;
+        }
+
+        @Override
+        protected void haltJvm(int status) {
+            this.halted = true;
+        }
+        
+        public boolean isExited() {
+            return this.exited;
+        }
+        
+        public boolean isHalted() {
+            return this.halted;
+        }
+    }
+    
 }