Interrupt command processing on client disconnect.
Summary:
Updates the version of Nailgun used by Buck to a fork which adds support for
detecting client disconnection and adds a callback to Main which interrupts
command processing on client disconnection, so that Buck behaves consistently
when killed wether using buckd or running standalone.
Test Plan: buck test --all
diff --git a/.classpath b/.classpath
index 924e3ee..e39204a 100644
--- a/.classpath
+++ b/.classpath
@@ -17,6 +17,7 @@
<classpathentry kind="lib" path="lib/jackson-annotations-2.0.5.jar"/>
<classpathentry kind="lib" path="lib/jsr305.jar"/>
<classpathentry kind="lib" path="lib/junit-4.11.jar" sourcepath="lib/junit-4.11-sources.jar"/>
+ <classpathentry kind="lib" path="lib/nailgun-server-0.9.2-SNAPSHOT.jar" sourcepath="lib/nailgun-server-0.9.2-SNAPSHOT-sources.jar"/>
<classpathentry kind="lib" path="lib/objenesis-1.2.jar"/>
<classpathentry kind="lib" path="lib/sdklib.jar"/>
<classpathentry kind="lib" path="third-party/java/asm/asm-debug-all-4.1.jar" sourcepath="third-party/java/asm/asm-4.1-src.zip"/>
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b22c943..9accd4d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -2,6 +2,11 @@
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Javac" />
+ <excludeFromCompile>
+ <directory url="file://$PROJECT_DIR$/buck-out/android" includeSubdirectories="true" />
+ <directory url="file://$PROJECT_DIR$/buck-out/bin" includeSubdirectories="true" />
+ <directory url="file://$PROJECT_DIR$/buck-out/gen" includeSubdirectories="true" />
+ </excludeFromCompile>
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="?*.properties" />
diff --git a/.idea/libraries/buck_lib.xml b/.idea/libraries/buck_lib.xml
index e147b05..957de19 100644
--- a/.idea/libraries/buck_lib.xml
+++ b/.idea/libraries/buck_lib.xml
@@ -11,11 +11,13 @@
<root url="jar://$PROJECT_DIR$/lib/sdklib.jar!/" />
<root url="jar://$PROJECT_DIR$/third-party/java/asm/asm-debug-all-4.1.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/guava-15.0.jar!/" />
+ <root url="jar://$PROJECT_DIR$/lib/nailgun-server-0.9.2-SNAPSHOT.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$PROJECT_DIR$/third-party/java/asm/asm-4.1-src.zip!/" />
<root url="jar://$PROJECT_DIR$/lib/guava-15.0-sources.jar!/" />
+ <root url="jar://$PROJECT_DIR$/lib/nailgun-server-0.9.2-SNAPSHOT-sources.jar!/" />
</SOURCES>
</library>
</component>
\ No newline at end of file
diff --git a/bin/buck b/bin/buck
index c2e6ec1..f9fe0f8 100755
--- a/bin/buck
+++ b/bin/buck
@@ -46,7 +46,6 @@
# Daemon is not running, or is busy so start buck process.
java \
$BUCK_JAVA_ARGS \
--Dbuck.daemon=false \
-classpath \
$BUCK_JAVA_CLASSPATH \
com.facebook.buck.cli.Main "$@"
diff --git a/bin/buck_common b/bin/buck_common
index c74a3b9..026a5ca 100755
--- a/bin/buck_common
+++ b/bin/buck_common
@@ -152,14 +152,14 @@
BUCK_JAVA_CLASSPATH="${BUCK_DIRECTORY}/src:\
${BUCK_DIRECTORY}/build/classes:\
${BUCK_DIRECTORY}/lib/args4j.jar:\
+${BUCK_DIRECTORY}/lib/ddmlib-r21.jar:\
${BUCK_DIRECTORY}/lib/guava-15.0.jar:\
${BUCK_DIRECTORY}/lib/ini4j-0.5.2.jar:\
${BUCK_DIRECTORY}/lib/jackson-annotations-2.0.5.jar:\
${BUCK_DIRECTORY}/lib/jackson-core-2.0.5.jar:\
${BUCK_DIRECTORY}/lib/jackson-databind-2.0.5.jar:\
${BUCK_DIRECTORY}/lib/jsr305.jar:\
-${BUCK_DIRECTORY}/lib/sdklib.jar:\
-${BUCK_DIRECTORY}/lib/ddmlib-r21.jar:\
+${BUCK_DIRECTORY}/lib/nailgun-server-0.9.2-SNAPSHOT.jar:\
${BUCK_DIRECTORY}/lib/sdklib.jar:\
${BUCK_DIRECTORY}/third-party/java/asm/asm-debug-all-4.1.jar:\
${BUCK_DIRECTORY}/third-party/java/astyanax/astyanax-cassandra-1.56.38.jar:\
diff --git a/bin/buckd b/bin/buckd
index 1401bdc..13bd1ec 100755
--- a/bin/buckd
+++ b/bin/buckd
@@ -38,10 +38,8 @@
# Run buckd.
java \
$BUCK_JAVA_ARGS \
--Dbuck.daemon=true \
-classpath \
-${BUCK_JAVA_CLASSPATH}:\
-${BUCK_DIRECTORY}/lib/nailgun-server-0.9.2-SNAPSHOT.jar \
+${BUCK_JAVA_CLASSPATH} \
com.martiansoftware.nailgun.NGServer localhost:${BUCKD_PORT} \
&> $BUCKD_LOG_FILE &
echo $! > "$BUCKD_PID_FILE"
diff --git a/build.xml b/build.xml
index 8ac1da4..7e075b7 100644
--- a/build.xml
+++ b/build.xml
@@ -30,6 +30,7 @@
<include name="jackson-databind-2.0.5.jar" />
<include name="jsr305.jar" />
<include name="sdklib.jar" />
+ <include name="nailgun-server-0.9.2-SNAPSHOT.jar" />
</fileset>
<fileset dir="${third-party.dir}" id="third-party.jars">
diff --git a/lib/BUCK b/lib/BUCK
index d960f9b..49d6f94 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -159,3 +159,14 @@
'//src/com/facebook/buck/android:steps',
]
)
+
+prebuilt_jar(
+ name = 'nailgun',
+ binary_jar = 'nailgun-server-0.9.2-SNAPSHOT.jar',
+ source_jar = 'nailgun-server-0.9.2-SNAPSHOT-sources.jar',
+ visibility = [
+ '//src/com/facebook/buck/cli:cli',
+ '//test/com/facebook/buck/cli:cli',
+ '//test/com/facebook/buck/testutil/integration:integration',
+ ]
+)
diff --git a/lib/README.txt b/lib/README.txt
index 88e850a..53963da 100644
--- a/lib/README.txt
+++ b/lib/README.txt
@@ -6,3 +6,18 @@
critical feature that we need for our purposes. For download go to:
http://downloads.sourceforge.net/project/emma/emma-release/2.0.5312/emma-2.0.5312-lib.zip
+
+
+nailgun-server-0.9.2-SNAPSHOT.jar and nailgun-server-0.9.2-SNAPSHOT-sources.jar were
+built from a fork of Nailgun which adds support for detecting client disconnection at
+https://github.com/jimpurbrick/nailgun.git
+
+To regenerate these jars:
+
+ 0) install maven (brew install maven)
+ 1) git clone https://github.com/jimpurbrick/nailgun.git
+ 2) cd nailgun
+ 3) git checkout d005c16f13d42489ac1ab428b15c3d9cdb1ada31
+ 4) mvn clean install
+ 5) copy nailgun-server/target/nailgun-server-0.9.2-SNAPSHOT.jar and
+ nailgun-server/target/nailgun-server-0.9.2-SNAPSHOT-sources.jar to your buck/lib directory
diff --git a/lib/nailgun-server-0.9.2-SNAPSHOT-sources.jar b/lib/nailgun-server-0.9.2-SNAPSHOT-sources.jar
new file mode 100644
index 0000000..a595667
--- /dev/null
+++ b/lib/nailgun-server-0.9.2-SNAPSHOT-sources.jar
Binary files differ
diff --git a/lib/nailgun-server-0.9.2-SNAPSHOT.jar b/lib/nailgun-server-0.9.2-SNAPSHOT.jar
index 5b5658e..8d45dda 100644
--- a/lib/nailgun-server-0.9.2-SNAPSHOT.jar
+++ b/lib/nailgun-server-0.9.2-SNAPSHOT.jar
Binary files differ
diff --git a/src/com/facebook/buck/cli/BUCK b/src/com/facebook/buck/cli/BUCK
index 6b8f806..c74794e 100644
--- a/src/com/facebook/buck/cli/BUCK
+++ b/src/com/facebook/buck/cli/BUCK
@@ -29,6 +29,7 @@
'//lib:jackson-core',
'//lib:jackson-databind',
'//lib:jsr305',
+ '//lib:nailgun',
'//src/com/facebook/buck/android:exceptions',
'//src/com/facebook/buck/event:event',
'//src/com/facebook/buck/command:command',
diff --git a/src/com/facebook/buck/cli/Main.java b/src/com/facebook/buck/cli/Main.java
index 5b62afa..22f65f0 100644
--- a/src/com/facebook/buck/cli/Main.java
+++ b/src/com/facebook/buck/cli/Main.java
@@ -55,6 +55,8 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.reflect.ClassPath;
+import com.martiansoftware.nailgun.NGClientListener;
+import com.martiansoftware.nailgun.NGContext;
import java.io.Closeable;
import java.io.File;
@@ -82,6 +84,11 @@
*/
public static final int BUSY_EXIT_CODE = 2;
+ /**
+ * Client disconnected.
+ */
+ public static final int CLIENT_DISCONNECT_EXIT_CODE = 3;
+
private static final String DEFAULT_BUCK_CONFIG_FILE_NAME = ".buckconfig";
private static final String DEFAULT_BUCK_CONFIG_OVERRIDE_FILE_NAME = ".buckconfig.local";
@@ -163,8 +170,32 @@
return parser;
}
+ private void watchClient(NGContext context) {
+ context.addClientListener(new NGClientListener() {
+ @Override
+ public void clientDisconnected() {
+
+ // Synchronize on parser object so that the main command processing thread is not
+ // interrupted mid way through a Parser cache update by the Thread.interrupt() call
+ // triggered by System.exit(). The Parser cache will be reused by subsequent commands
+ // so needs to be left in a consistent state even if the current command is interrupted
+ // due to a client disconnection.
+ synchronized (parser) {
+ System.exit(CLIENT_DISCONNECT_EXIT_CODE);
+ }
+ }
+ });
+ }
+
private void watchFileSystem() throws IOException {
- filesystemWatcher.postEvents();
+
+ // Synchronize on parser object so that all outstanding watch events are processed
+ // as a single, atomic Parser cache update and are not interleaved with Parser cache
+ // invalidations triggered by requests to parse build files or interrupted by client
+ // disconnections.
+ synchronized (parser) {
+ filesystemWatcher.postEvents();
+ }
}
/** @return true if the web server was started successfully. */
@@ -203,10 +234,16 @@
@Nullable private static Daemon daemon;
- private boolean isDaemon() {
- return Boolean.getBoolean("buck.daemon");
+ /**
+ * Get existing Daemon.
+ */
+ private Daemon getDaemon() {
+ return Preconditions.checkNotNull(daemon);
}
+ /**
+ * Get or create Daemon.
+ */
private Daemon getDaemon(ProjectFilesystem filesystem,
BuckConfig config,
Console console) throws IOException {
@@ -273,11 +310,12 @@
}
/**
+ * @param context an optional NGContext that is present if running inside a Nailgun server.
* @param args command line arguments
* @return an exit code or {@code null} if this is a process that should not exit
*/
@SuppressWarnings("PMD.EmptyCatchBlock")
- public int runMainWithExitCode(File projectRoot, String... args) throws IOException {
+ public int runMainWithExitCode(File projectRoot, Optional<NGContext> context, String... args) throws IOException {
if (args.length == 0) {
return usage();
}
@@ -295,15 +333,13 @@
// Create or get and invalidate cached command parameters.
Parser parser;
- Optional<Daemon> daemonOptional;
- if (isDaemon()) {
+ if (context.isPresent()) {
Daemon daemon = getDaemon(projectFilesystem, config, console);
+ daemon.watchClient(context.get());
daemon.watchFileSystem();
daemon.initWebServer();
- daemonOptional = Optional.of(daemon);
parser = daemon.getParser();
} else {
- daemonOptional = Optional.absent();
parser = new Parser(projectFilesystem,
knownBuildRuleTypes,
console,
@@ -321,24 +357,20 @@
// Find and execute command.
Optional<Command> command = Command.getCommandForName(args[0], console);
if (command.isPresent()) {
- Optional<WebServer> webServerOptional = Optional.absent();
- if (daemonOptional.isPresent()) {
- webServerOptional = daemonOptional.get().getWebServer();
- }
ImmutableList<BuckEventListener> eventListeners =
addEventListeners(buildEventBus,
clock,
projectFilesystem,
console,
config,
- webServerOptional);
+ getWebServerIfDaemon(context));
String[] remainingArgs = new String[args.length - 1];
System.arraycopy(args, 1, remainingArgs, 0, remainingArgs.length);
Command executingCommand = command.get();
String commandName = executingCommand.name().toLowerCase();
- buildEventBus.post(CommandEvent.started(commandName, isDaemon()));
+ buildEventBus.post(CommandEvent.started(commandName, context.isPresent()));
// The ArtifactCache is constructed lazily so that we do not try to connect to Cassandra when
// running commands such as `buck clean`.
@@ -366,7 +398,7 @@
parser,
platform));
- buildEventBus.post(CommandEvent.finished(commandName, isDaemon(), exitCode));
+ buildEventBus.post(CommandEvent.finished(commandName, context.isPresent(), exitCode));
ExecutorService buildEventBusExecutor = buildEventBus.getExecutorService();
buildEventBusExecutor.shutdown();
@@ -390,6 +422,13 @@
}
}
+ private Optional<WebServer> getWebServerIfDaemon(Optional<NGContext> context) {
+ if (context.isPresent()) {
+ return getDaemon().getWebServer();
+ }
+ return Optional.absent();
+ }
+
private void loadListenersFromBuckConfig(
ImmutableList.Builder<BuckEventListener> eventListeners,
ProjectFilesystem projectFilesystem,
@@ -522,13 +561,14 @@
}
@VisibleForTesting
- int tryRunMainWithExitCode(File projectRoot, String... args) throws IOException {
+ int tryRunMainWithExitCode(File projectRoot, Optional<NGContext> context, String... args)
+ throws IOException {
// TODO(user): enforce write command exclusion, but allow concurrent read only commands?
if (!commandSemaphore.tryAcquire()) {
return BUSY_EXIT_CODE;
}
try {
- return runMainWithExitCode(projectRoot, args);
+ return runMainWithExitCode(projectRoot, context, args);
} catch (HumanReadableException e) {
Console console = new Console(Verbosity.STANDARD_INFORMATION,
stdOut,
@@ -541,12 +581,11 @@
}
}
- public static void main(String[] args) {
- Main main = new Main(System.out, System.err);
+ private void runMainThenExit(String[] args, Optional<NGContext> context) {
File projectRoot = new File(".");
int exitCode = FAIL_EXIT_CODE;
try {
- exitCode = main.tryRunMainWithExitCode(projectRoot, args);
+ exitCode = tryRunMainWithExitCode(projectRoot, context, args);
} catch (Throwable t) {
t.printStackTrace();
} finally {
@@ -555,4 +594,17 @@
System.exit(exitCode);
}
}
+
+ public static void main(String[] args) {
+ new Main(System.out, System.err).runMainThenExit(args, Optional.<NGContext>absent());
+ }
+
+ /**
+ * When running as a daemon in the NailGun server, {@link #nailMain(NGContext)} is called instead
+ * of {@link #main(String[])} so that the given context can be used to listen for client
+ * disconnections and interrupt command processing when they occur.
+ */
+ public static void nailMain(final NGContext context) throws InterruptedException {
+ new Main(context.out, context.err).runMainThenExit(context.getArgs(), Optional.of(context));
+ }
}
diff --git a/src/com/facebook/buck/parser/Parser.java b/src/com/facebook/buck/parser/Parser.java
index 4dc3d8a..4593454 100644
--- a/src/com/facebook/buck/parser/Parser.java
+++ b/src/com/facebook/buck/parser/Parser.java
@@ -64,7 +64,6 @@
import java.util.regex.Pattern;
import javax.annotation.Nullable;
-import javax.annotation.concurrent.NotThreadSafe;
/**
* High-level build file parsing machinery. Primarily responsible for producing a
@@ -73,7 +72,6 @@
* processes filesystem WatchEvents to invalidate the cache as files change. Expected to be used
* from a single thread, so methods are not synchronized or thread safe.
*/
-@NotThreadSafe
public class Parser {
private final BuildTargetParser buildTargetParser;
@@ -250,7 +248,7 @@
* @param includes the files to include before executing the build file.
* @return true if the cache was invalidated, false if the cache is still valid.
*/
- private boolean invalidateCacheOnIncludeChange(Iterable<String> includes) {
+ private synchronized boolean invalidateCacheOnIncludeChange(Iterable<String> includes) {
List<String> includesList = Lists.newArrayList(includes);
if (!includesList.equals(this.cacheDefaultIncludes)) {
invalidateCache();
@@ -260,7 +258,7 @@
return false;
}
- private void invalidateCache() {
+ private synchronized void invalidateCache() {
if (console.getVerbosity() == Verbosity.ALL) {
console.getStdErr().println("Parser invalidating entire cache");
}
@@ -461,7 +459,7 @@
* @param rules the raw rule objects to parse.
*/
@VisibleForTesting
- void parseRawRulesInternal(Iterable<Map<String, Object>> rules)
+ synchronized void parseRawRulesInternal(Iterable<Map<String, Object>> rules)
throws BuildTargetException, IOException {
for (Map<String, Object> map : rules) {
@@ -577,7 +575,7 @@
* in the List returned by this method. If filter is null, then this method returns null.
* @return The build targets in the project filtered by the given filter.
*/
- public List<BuildTarget> filterAllTargetsInProject(ProjectFilesystem filesystem,
+ public synchronized List<BuildTarget> filterAllTargetsInProject(ProjectFilesystem filesystem,
Iterable<String> includes,
@Nullable RawRulePredicate filter)
throws BuildFileParseException, BuildTargetException, IOException {
@@ -676,7 +674,7 @@
* targets and rules defined by files that transitively include {@code path} from the cache.
* @param path The File that has changed.
*/
- private void invalidateDependents(Path path) {
+ private synchronized void invalidateDependents(Path path) {
// Normalize path to ensure it hashes equally with map keys.
path = normalize(path);
diff --git a/test/com/facebook/buck/cli/BUCK b/test/com/facebook/buck/cli/BUCK
index 065fe05..be5721d 100644
--- a/test/com/facebook/buck/cli/BUCK
+++ b/test/com/facebook/buck/cli/BUCK
@@ -68,6 +68,7 @@
'//lib:jackson-databind',
'//lib:jsr305',
'//lib:junit',
+ '//lib:nailgun',
'//src/com/facebook/buck/android:rules',
'//src/com/facebook/buck/cli:cli',
'//src/com/facebook/buck/cli:events',
diff --git a/test/com/facebook/buck/cli/DaemonIntegrationTest.java b/test/com/facebook/buck/cli/DaemonIntegrationTest.java
index 5f42a38..32f9020 100644
--- a/test/com/facebook/buck/cli/DaemonIntegrationTest.java
+++ b/test/com/facebook/buck/cli/DaemonIntegrationTest.java
@@ -17,20 +17,35 @@
package com.facebook.buck.cli;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import com.facebook.buck.testutil.integration.DebuggableTemporaryFolder;
import com.facebook.buck.testutil.integration.ProjectWorkspace;
import com.facebook.buck.testutil.integration.TestDataHelper;
import com.facebook.buck.util.CapturingPrintStream;
+import com.google.common.base.Optional;
import com.google.common.base.Throwables;
+import com.martiansoftware.nailgun.NGClientListener;
+import com.martiansoftware.nailgun.NGConstants;
+import com.martiansoftware.nailgun.NGContext;
+import com.martiansoftware.nailgun.NGExitException;
+import com.martiansoftware.nailgun.NGInputStream;
+import com.martiansoftware.nailgun.NGSecurityManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -39,6 +54,7 @@
public class DaemonIntegrationTest {
+ private static final int SUCCESS_EXIT_CODE = 0;
private ScheduledExecutorService executorService;
@Rule
@@ -55,9 +71,9 @@
}
/**
- * This verifies that when the user tries to run the Buck Main method, while it is already running,
- * the second call will fail to avoid multiple threads accessing and corrupting the static state
- * used by the Buck daemon.
+ * This verifies that when the user tries to run the Buck Main method, while it is already
+ * running, the second call will fail. Serializing command execution in this way avoids
+ * multiple threads accessing and corrupting the static state used by the Buck daemon.
*/
@Test
public void testExclusiveExecution()
@@ -75,8 +91,11 @@
public void run() {
try {
Main main = new Main(stdOut, firstThreadStdErr);
- int exitCode = main.tryRunMainWithExitCode(tmp.getRoot(), "build", "//:sleep");
- assertEquals("Should return 0 when no command running.", 0, exitCode);
+ int exitCode = main.tryRunMainWithExitCode(tmp.getRoot(),
+ Optional.<NGContext>absent(),
+ "build",
+ "//:sleep");
+ assertEquals("Should return 0 when no command running.", SUCCESS_EXIT_CODE, exitCode);
} catch (IOException e) {
fail("Should not throw IOException");
throw Throwables.propagate(e);
@@ -88,8 +107,10 @@
public void run() {
try {
Main main = new Main(stdOut, secondThreadStdErr);
- int exitCode = main.tryRunMainWithExitCode(tmp.getRoot(), "targets");
- assertEquals("Should return 1 when command running.", Main.BUSY_EXIT_CODE, exitCode);
+ int exitCode = main.tryRunMainWithExitCode(tmp.getRoot(),
+ Optional.<NGContext>absent(),
+ "targets");
+ assertEquals("Should return 2 when command running.", Main.BUSY_EXIT_CODE, exitCode);
} catch (IOException e) {
fail("Should not throw IOException.");
throw Throwables.propagate(e);
@@ -99,4 +120,75 @@
firstThread.get();
secondThread.get();
}
+
+ private InputStream createHeartbeatStream(int count) {
+ final int BYTES_PER_HEARTBEAT = 5;
+ byte[] bytes = new byte[BYTES_PER_HEARTBEAT * count];
+ Arrays.fill(bytes, NGConstants.CHUNKTYPE_HEARTBEAT);
+ return new ByteArrayInputStream(bytes);
+ }
+
+ /**
+ * This verifies that a client disconnection will be detected by a Nailgun
+ * NGInputStream which then calls a clientDisconnected handler which interrupts Buck command
+ * processing.
+ */
+ @Test
+ public void whenClientDisconnectsThenCommandIsInterrupted()
+ throws InterruptedException, IOException {
+
+ // NGInputStream test double which provides access to registered client listener.
+ class TestNGInputStream extends NGInputStream {
+
+ public NGClientListener listener = null;
+
+ public TestNGInputStream(InputStream in, DataOutputStream out, PrintStream serverLog) {
+ super(in, out, serverLog);
+ }
+
+ @Override
+ public synchronized void addClientListener(NGClientListener listener) {
+ this.listener = listener;
+ }
+ }
+
+ // Build an NGContext connected to an NGInputStream reading from a stream of heartbeats.
+ Thread.currentThread().setName("Test");
+ CapturingPrintStream serverLog = new CapturingPrintStream();
+ NGContext context = new NGContext();
+ TestNGInputStream inputStream = new TestNGInputStream(
+ new DataInputStream(createHeartbeatStream(100)),
+ new DataOutputStream(new ByteArrayOutputStream(0)),
+ serverLog);
+ context.setArgs(new String[] {"targets"});
+ context.in = inputStream;
+ context.out = new CapturingPrintStream();
+ context.err = new CapturingPrintStream();
+
+ // NGSecurityManager is used to convert System.exit() calls in to NGExitExceptions.
+ SecurityManager originalSecurityManager = System.getSecurityManager();
+
+ // Run command to register client listener.
+ try {
+ System.setSecurityManager(new NGSecurityManager(originalSecurityManager));
+ Main.nailMain(context);
+ fail("Should throw NGExitException.");
+ } catch (NGExitException e) {
+ assertEquals("Should exit with status 0.", SUCCESS_EXIT_CODE, e.getStatus());
+ } finally {
+ System.setSecurityManager(originalSecurityManager);
+ }
+
+ // Check listener was registered calls System.exit() with client disconnect exit code.
+ try {
+ System.setSecurityManager(new NGSecurityManager(originalSecurityManager));
+ assertNotNull("Should register client listener.", inputStream.listener);
+ inputStream.listener.clientDisconnected();
+ fail("Should throw NGExitException.");
+ } catch (NGExitException e) {
+ assertEquals("Should exit with status 3", Main.CLIENT_DISCONNECT_EXIT_CODE, e.getStatus());
+ } finally {
+ System.setSecurityManager(originalSecurityManager);
+ }
+ }
}
diff --git a/test/com/facebook/buck/cli/MainTest.java b/test/com/facebook/buck/cli/MainTest.java
index 7ff533d..bbccee2 100644
--- a/test/com/facebook/buck/cli/MainTest.java
+++ b/test/com/facebook/buck/cli/MainTest.java
@@ -21,6 +21,8 @@
import com.facebook.buck.util.CapturingPrintStream;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.martiansoftware.nailgun.NGContext;
import org.easymock.EasyMock;
import org.junit.After;
@@ -62,7 +64,7 @@
CapturingPrintStream stdErr = new CapturingPrintStream();
Main main = new Main(stdOut, stdErr);
- int exitCode = main.runMainWithExitCode(new File("."));
+ int exitCode = main.runMainWithExitCode(new File("."), Optional.<NGContext>absent());
assertEquals(1, exitCode);
assertEquals(
"When the user does not specify any arguments, the usage information should be displayed",
@@ -75,7 +77,7 @@
CapturingPrintStream stdErr = new CapturingPrintStream();
Main main = new Main(stdOut, stdErr);
- int exitCode = main.runMainWithExitCode(new File("."), "--help");
+ int exitCode = main.runMainWithExitCode(new File("."), Optional.<NGContext>absent(), "--help");
assertEquals(1, exitCode);
assertEquals("Users instinctively try running `buck --help`, so it should print usage info.",
getUsageString(),
diff --git a/test/com/facebook/buck/testutil/integration/BUCK b/test/com/facebook/buck/testutil/integration/BUCK
index d5e8568..669980a 100644
--- a/test/com/facebook/buck/testutil/integration/BUCK
+++ b/test/com/facebook/buck/testutil/integration/BUCK
@@ -10,6 +10,7 @@
'//lib:guava',
'//lib:jsr305',
'//lib:junit',
+ '//lib:nailgun',
],
visibility = [
'PUBLIC',
diff --git a/test/com/facebook/buck/testutil/integration/ProjectWorkspace.java b/test/com/facebook/buck/testutil/integration/ProjectWorkspace.java
index 3d682d4..822fc1f 100644
--- a/test/com/facebook/buck/testutil/integration/ProjectWorkspace.java
+++ b/test/com/facebook/buck/testutil/integration/ProjectWorkspace.java
@@ -27,8 +27,10 @@
import com.facebook.buck.util.environment.Platform;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
+import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
+import com.martiansoftware.nailgun.NGContext;
import org.junit.rules.TemporaryFolder;
@@ -129,7 +131,6 @@
};
java.nio.file.Files.walkFileTree(destPath, copyDirVisitor);
}
-
isSetUp = true;
}
@@ -145,7 +146,7 @@
CapturingPrintStream stderr = new CapturingPrintStream();
Main main = new Main(stdout, stderr);
- int exitCode = main.runMainWithExitCode(destDir, args);
+ int exitCode = main.runMainWithExitCode(destDir, Optional.<NGContext>absent(), args);
return new ProcessResult(exitCode,
stdout.getContentsAsString(Charsets.UTF_8),
diff --git a/third-party/nailgun/README.md b/third-party/nailgun/README.md
index 4e6301f..213d4f4 100644
--- a/third-party/nailgun/README.md
+++ b/third-party/nailgun/README.md
@@ -15,3 +15,7 @@
you will additionally need to "make ng.exe".
For more information, see [the nailgun website](http://martiansoftware.com/nailgun/).
+
+Buck currently uses a fork of nailgun hosted at https://github.com/jimpurbrick/nailgun.
+This fork adds support for interrupting server processing when client disconnection is
+detected, changes which are currently being accepted in to the main nailgun repository.
\ No newline at end of file
diff --git a/third-party/nailgun/nailgun-client/ng.c b/third-party/nailgun/nailgun-client/ng.c
index 5a34aef..bd7a390 100644
--- a/third-party/nailgun/nailgun-client/ng.c
+++ b/third-party/nailgun/nailgun-client/ng.c
@@ -27,6 +27,7 @@
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
+ #include <sys/time.h>
#include <sys/types.h>
#endif
@@ -92,7 +93,8 @@
#define CHUNKTYPE_DIR 'D'
#define CHUNKTYPE_CMD 'C'
#define CHUNKTYPE_EXIT 'X'
-#define CHUNKTYPE_STARTINPUT 'S'
+#define CHUNKTYPE_SENDINPUT 'S'
+#define CHUNKTYPE_HEARTBEAT 'H'
/*
the following is required to compile for hp-ux
@@ -108,8 +110,8 @@
/* buffer used for receiving and writing nail output chunks */
char buf[BUFSIZE];
-/* track whether or not we've been told to send stdin to server */
-int startedInput = 0;
+/* track whether server is ready to receive */
+int readyToSend = 0;
/**
* Clean up the application.
@@ -334,6 +336,7 @@
* @param len the number of bytes to send
*/
void sendStdin(char *buf, unsigned int len) {
+ readyToSend = 0;
sendHeader(len, CHUNKTYPE_STDIN);
sendAll(nailgunsocket, buf, len);
}
@@ -345,6 +348,12 @@
sendHeader(0, CHUNKTYPE_STDIN_EOF);
}
+/**
+ * Sends a heartbeat chunk to let the server know the client is still alive.
+ */
+void sendHeartbeat() {
+ sendHeader(0, CHUNKTYPE_HEARTBEAT);
+}
#ifdef WIN32
/**
@@ -357,7 +366,7 @@
for (;;) {
DWORD numberOfBytes = 0;
- if (!ReadFile(NG_STDIN_FILENO, wbuf, BUFSIZE, &numberOfBytes, NULL)) {
+ if (readyToSend && !ReadFile(NG_STDIN_FILENO, wbuf, BUFSIZE, &numberOfBytes, NULL)) {
if (numberOfBytes != 0) {
handleError();
}
@@ -464,13 +473,8 @@
break;
case CHUNKTYPE_EXIT: processExit(buf, len);
break;
- case CHUNKTYPE_STARTINPUT:
- if (!startedInput) {
- #ifdef WIN32
- winStartInput();
- #endif
- startedInput = 1;
- }
+ case CHUNKTYPE_SENDINPUT:
+ readyToSend = 1;
break;
default: fprintf(stderr, "Unexpected chunk type %d ('%c')\n", chunkType, chunkType);
cleanUpAndExit(NAILGUN_UNEXPECTED_CHUNKTYPE);
@@ -551,6 +555,8 @@
#ifndef WIN32
fd_set readfds;
int eof = 0;
+ struct timeval readtimeout;
+
#endif
#ifdef WIN32
@@ -696,24 +702,29 @@
FD_ZERO(&readfds);
/* don't select on stdin if we've already reached its end */
- if (startedInput && !eof) {
+ if (readyToSend && !eof) {
FD_SET(NG_STDIN_FILENO, &readfds);
}
FD_SET(nailgunsocket, &readfds);
- if (select (nailgunsocket + 1, &readfds, NULL, NULL, NULL) == -1) {
- perror("select");
+
+ memset(&readtimeout, '\0', sizeof(readtimeout));
+ readtimeout.tv_usec = 100000;
+ if(select (nailgunsocket + 1, &readfds, NULL, NULL, &readtimeout) == -1) {
+ perror("select");
}
if (FD_ISSET(nailgunsocket, &readfds)) {
#endif
- processnailgunstream();
+ processnailgunstream();
#ifndef WIN32
} else if (FD_ISSET(NG_STDIN_FILENO, &readfds)) {
- if (!processStdin()) {
- FD_CLR(NG_STDIN_FILENO, &readfds);
- eof = 1;
- }
+ if (!processStdin()) {
+ FD_CLR(NG_STDIN_FILENO, &readfds);
+ eof = 1;
+ }
+ } else {
+ sendHeartbeat();
}
#endif
}