org.gradle.launcher.daemon.ProcessCrashHandlingIntegrationTest.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-api Show documentation
Show all versions of gradle-api Show documentation
Gradle 6.9.1 API redistribution.
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.launcher.daemon
import org.gradle.integtests.fixtures.daemon.DaemonClientFixture
import org.gradle.integtests.fixtures.daemon.DaemonIntegrationSpec
import org.gradle.launcher.daemon.client.DaemonDisappearedException
import org.gradle.launcher.daemon.logging.DaemonMessages
import org.gradle.test.fixtures.server.http.CyclicBarrierHttpServer
import org.gradle.util.Requires
import org.gradle.util.TestPrecondition
import org.junit.Rule
class ProcessCrashHandlingIntegrationTest extends DaemonIntegrationSpec {
@Rule CyclicBarrierHttpServer server = new CyclicBarrierHttpServer()
def "tears down the daemon process when the client disconnects and build does not cancel in a timely manner"() {
buildFile << """
task block {
doLast {
new URL("$server.uri").text
}
}
"""
when:
def client = new DaemonClientFixture(executer.withArgument("--debug").withTasks("block").start())
server.waitFor()
daemons.daemon.assertBusy()
client.kill()
then:
daemons.daemon.becomesCanceled()
and:
daemons.daemon.stops()
}
def "daemon is idle after the client disconnects and build cancels in a timely manner"() {
buildFile << """
task block {
doLast {
new URL("$server.uri").text
}
}
"""
when:
def client = new DaemonClientFixture(executer.withArgument("--debug").withTasks("block").start())
server.waitFor()
daemons.daemon.assertBusy()
client.kill()
then:
daemons.daemon.becomesCanceled()
when:
server.release()
then:
daemons.daemon.becomesIdle()
and:
daemons.daemon.log.contains(DaemonMessages.CANCELED_BUILD)
}
/**
* When the daemon is started on *nix, we need to detach it from the terminal session of the parent process,
* otherwise if a ctrl-c is entered on the terminal, it will kill all processes in the session. This test compiles
* a native executable that can retrieve the session id of a process so that we can verify that the session id
* of the daemon is different than the session id of the client.
*/
@Requires(TestPrecondition.NOT_WINDOWS)
def "session id of daemon is different from daemon client"() {
given:
withGetSidProject()
succeeds(":getSid:install")
buildFile << """
task block {
doLast {
new URL("$server.uri").text
}
}
"""
when:
DaemonClientFixture client = new DaemonClientFixture(executer.withArgument("--debug").withTasks("block").start())
server.waitFor()
daemons.daemon.assertBusy()
then:
def clientSid = getSid(client.process.pid)
def daemonSid = getSid(daemons.daemon.context.pid)
clientSid != daemonSid
cleanup:
server.release()
}
/**
* When the daemon is started on Windows, we need to detach from the console that started the process, otherwise if
* a ctrl-c is entered in that console, it will send a kill signal to all processes attached to the console. This
* test compiles a native executable that attempts to connect to the console of a given pid. We use this to verify
* that the daemon is not attached to any console. Note that this is the only way to validate this in Windows since
* there is no way to determine which console (if any) a process is attached to. We really only have a system call
* that allows us to attach to the same console some other process is attached to. If that process is not attached
* to any console, we get a specific error that we check for.
*/
@Requires(TestPrecondition.WINDOWS)
def "daemon is not attached to a console"() {
given:
withAttachConsoleProject()
succeeds(":attachConsole:install")
buildFile << """
task block {
doLast {
new URL("$server.uri").text
}
}
"""
when:
executer.withTasks("block").start()
server.waitFor()
daemons.daemon.assertBusy()
then:
getConsole(daemons.daemon.context.pid) == "none"
cleanup:
server.release()
}
def "client logs useful information when daemon crashes"() {
buildFile << """
task block {
doLast {
new URL("$server.uri").text
}
}
"""
when:
executer.withStackTraceChecksDisabled() // daemon log may contain stack traces
def build = executer.withTasks("block").start()
server.waitFor()
daemons.daemon.kill()
def failure = build.waitForFailure()
then:
failure.error.contains("----- Last 20 lines from daemon log file")
failure.assertHasDescription(DaemonDisappearedException.MESSAGE)
}
def "client logs useful information when daemon exits"() {
given:
file("build.gradle") << "System.exit(0)"
when:
executer.withStackTraceChecksDisabled() // daemon log may contain stack traces
def failure = executer.runWithFailure()
then:
failure.error.contains("----- Last 20 lines from daemon log file")
failure.error.contains(DaemonMessages.DAEMON_VM_SHUTTING_DOWN)
failure.assertHasDescription(DaemonDisappearedException.MESSAGE)
and:
daemons.daemon.stops()
}
String getSid(Long pid) {
return file("getSid/build/install/getSid/lib/getSid").exec(pid as String).out.trim()
}
String getConsole(Long pid) {
return file("attachConsole/build/install/attachConsole/attachConsole.bat").exec(pid as String).out.trim()
}
void withAttachConsoleProject() {
withProject("attachConsole", """
#include
#include
#include
// Convenience method for getting a human readable form of the last error.
void ErrorExit(const char *func){
DWORD errCode = GetLastError();
char *err;
if (!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
errCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // default language
(LPTSTR) &err,
0,
NULL))
return;
static char buffer[1024];
_snprintf(buffer, sizeof(buffer), "ERROR: %s failed with error: %s\\n", func, err);
printf("%s", buffer);
exit(1);
}
int main (int argc, char *argv[]) {
long pid;
if (argc != 2) {
printf("Expected a pid parameter, but was given %d\\\\n", argc - 1);
return 1;
} else {
char *ptr;
pid = strtoul(argv[1], &ptr, 10);
}
// We cannot attach to more than one console at a time, so we
// need to make sure we are detached first
if (!FreeConsole()) {
if (GetLastError() != ERROR_INVALID_PARAMETER) {
ErrorExit(TEXT("FreeConsole"));
}
}
// AttachConsole also returns ERROR_GEN_FAILURE when the process
// doesn't exist, so we need to verify that the pid is valid.
if (OpenProcess(SYNCHRONIZE, FALSE, pid) == NULL) {
ErrorExit(TEXT("OpenProcess"));
}
// We expect AttachConsole to fail with a particular error if the
// provided pid is not attached to a console
if (!AttachConsole(pid)) {
if (GetLastError() == ERROR_GEN_FAILURE) {
printf("none\\n");
exit(0);
} else {
ErrorExit(TEXT("AttachConsole"));
}
}
// Otherwise we return the pid to signify that we were able to attach to its console.
// In the context of the test, this would be considered a "failure" as we expect to find
// the daemon not to have a console that we can attach to.
printf("%d\\n", pid);
exit(0);
}
""")
}
void withGetSidProject() {
withProject("getSid", """
#include
#include
#include
int main (int argc, char *argv[]) {
int pid, sid;
if (argc != 2) {
printf("Expected a pid parameter, but was given %d\\n", argc - 1);
return 1;
} else {
pid = atoi(argv[1]);
}
pid_t getsid(pid_t pid);
sid = getsid(pid);
printf("%d\\n", sid);
return 0;
}
""")
}
void withProject(exeName, source) {
file("${exeName}/src/${exeName}/c/${exeName}.c") << source
file("${exeName}/build.gradle") << """
apply plugin: 'org.gradle.c'
model {
components {
${exeName}(NativeExecutableSpec) {
}
}
}
"""
file('settings.gradle') << """
include(':${exeName}')
"""
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy