Skip to content
This repository was archived by the owner on Feb 20, 2025. It is now read-only.

Commit 2e803b3

Browse files
Improve process termination on cleanDanglingUnityFiles (#205)
## Description Relying on the output of `processHandle.destroy()` is only enough to know if the process received the TERM signal, not if it did anything with it. We now check if the process is actually terminated (5s timeout) before pulling the big guns and sending a KILL. Also add lots of logs for people to know exactly whats going on here. ## Changes * ![FIX] `terminateProcess` option on `clearDanglingUnityFiles` task wasn't terminating frozen processes * ![IMPROVE] lots of logs for `clearDanglingUnityFiles` [NEW]: https://resources.atlas.wooga.com/icons/icon_new.svg "New" [ADD]: https://resources.atlas.wooga.com/icons/icon_add.svg "Add" [IMPROVE]: https://resources.atlas.wooga.com/icons/icon_improve.svg "Improve" [CHANGE]: https://resources.atlas.wooga.com/icons/icon_change.svg "Change" [FIX]: https://resources.atlas.wooga.com/icons/icon_fix.svg "Fix" [UPDATE]: https://resources.atlas.wooga.com/icons/icon_update.svg "Update" [BREAK]: https://resources.atlas.wooga.com/icons/icon_break.svg "Remove" [REMOVE]: https://resources.atlas.wooga.com/icons/icon_remove.svg "Remove" [IOS]: https://resources.atlas.wooga.com/icons/icon_iOS.svg "iOS" [ANDROID]: https://resources.atlas.wooga.com/icons/icon_android.svg "Android" [WEBGL]: https://resources.atlas.wooga.com/icons/icon_webGL.svg "WebGL" [GRADLE]: https://resources.atlas.wooga.com/icons/icon_gradle.svg "GRADLE" [UNITY]: https://resources.atlas.wooga.com/icons/icon_unity.svg "Unity" [LINUX]: https://resources.atlas.wooga.com/icons/icon_linux.svg "Linux" [WIN]: https://resources.atlas.wooga.com/icons/icon_windows.svg "Windows" [MACOS]: https://resources.atlas.wooga.com/icons/icon_iOS.svg "macOS"
1 parent 327f0a8 commit 2e803b3

File tree

2 files changed

+106
-38
lines changed

2 files changed

+106
-38
lines changed

src/integrationTest/groovy/wooga/gradle/unity/tasks/ClearDanglingUnityFilesIntegrationSpec.groovy

+75-32
Original file line numberDiff line numberDiff line change
@@ -47,55 +47,98 @@ class ClearDanglingUnityFilesIntegrationSpec extends IntegrationSpec {
4747
assert tempFolder.exists()
4848
assert fakeProcess ? fakeProcess.alive : true
4949
assert editorInstanceFile.exists() == hasEditorInstanceFile
50-
runTasksSuccessfully("testTask")
50+
def result = runTasksSuccessfully("testTask")
5151

5252
then:
53+
println result.standardOutput
5354
tempFolder.exists() == !shouldCleanup
5455

5556
cleanup:
56-
fakeProcess?.destroy()
57+
fakeProcess?.destroyForcibly()
5758
editorInstanceFile.delete()
5859

5960

6061
where:
6162
hasOpenProcess | hasEditorInstanceFile | terminateProcess | shouldCleanup | prefix | suffix
62-
false | false | false | true | "should delete" | "no running Unity process with the project [0]"
63-
false | false | true | true | "should delete" | "no running Unity process with the project [1]"
64-
true | true | true | true | "should delete" | "running process with the project [1]"
65-
true | false | false | true | "should delete" | "running process with the project [2]"
66-
true | true | false | false | "shouldnt delete" | "running process with the project"
63+
false | false | false | true | "should delete" | "no running Unity process with the project [terminate=false]"
64+
false | false | true | true | "should delete" | "no running Unity process with the project [terminate=true]"
65+
true | true | true | true | "should delete" | "running process with the project [terminate=true, EditorInstance.json]"
66+
true | false | false | true | "should delete" | "running process with the project [terminate=false, no EditorInstance.json]"
67+
true | true | false | false | "shouldnt delete" | "running process with the project [terminate=false, EditorInstance.json]"
6768
}
6869

69-
def createFakeFrozenUnityExecutable() {
70+
@IgnoreIf({ os.windows })
71+
def "#prefix terminate Unity process if there is a #suffix"() {
72+
given:
73+
def tempFolder = new File(projectDir, "Temp").with {
74+
it.mkdir()
75+
new File(it, "UnityLockfile").createNewFile()
76+
return it
77+
}
78+
and:
79+
def fakeUnityExec = createFakeFrozenUnityExecutable(processIgnoring)
80+
def fakeProcess = "$fakeUnityExec.absolutePath -projectPath ${projectDir.absolutePath}".execute()
81+
fakeProcess.waitFor(10, TimeUnit.MILLISECONDS)
82+
File editorInstanceFile = new File(projectDir, "Library/EditorInstance.json")
83+
editorInstanceFile.parentFile.mkdir()
84+
editorInstanceFile << """
85+
{
86+
"process_id" : ${wrapValueBasedOnType(fakeProcess.pid(), Integer)},
87+
"version" : "any",
88+
"app_path" : "any",
89+
"app_contents_path" : "any"
90+
}
91+
"""
92+
and:
93+
buildFile << """
94+
tasks.register("testTask", wooga.gradle.unity.tasks.ClearDanglingUnityFiles) {
95+
projectDirectory.set(${wrapValueBasedOnType(projectDir, File)})
96+
terminateOpenProcess.set($terminateProcess)
97+
}
98+
"""
99+
100+
when:
101+
assert tempFolder.exists()
102+
assert fakeProcess ? fakeProcess.alive : true
103+
def result = runTasksSuccessfully("testTask")
104+
105+
then:
106+
println result.standardOutput
107+
terminateProcess == !fakeProcess.alive
108+
109+
cleanup:
110+
fakeProcess?.destroyForcibly()
111+
editorInstanceFile.delete()
112+
113+
114+
where:
115+
terminateProcess | processIgnoring | prefix | suffix
116+
false | [] | "shouldn't" | "normal running process"
117+
false | ["SIGINT", "SIGTERM"] | "shouldn't" | "frozen process"
118+
true | [] | "should" | "normal running process"
119+
true | ["SIGINT", "SIGTERM"] | "should" | "frozen process"
120+
}
121+
122+
def createFakeFrozenUnityExecutable(List<String> signalsToIgnore = []) {
70123
def fakeFrozenUnity = new File(projectDir, "Unity").with {
71124
it.createNewFile()
72125
it.executable = true
73126
return it
74127
}
75-
if (PlatformUtils.windows) {
76-
fakeFrozenUnity <<
77-
"""@echo off
78-
echo 'started'
79-
:loop
80-
timeout /t 0.010 >nul
81-
goto loop
82-
"""
83-
} else {
84-
fakeFrozenUnity <<
85-
"""
86-
#!/bin/bash
87-
88-
# Trap the SIGINT signal (Ctrl+C) and execute a function
89-
trap 'exit' SIGINT
90-
91-
echo "started"
92-
# Infinite loop
93-
while true
94-
do
95-
sleep 0.010
96-
done
97-
"""
98-
}
128+
fakeFrozenUnity <<
129+
"""
130+
#!/bin/bash
131+
132+
# Trap the SIGINT signal (Ctrl+C) and SIGTERM (kill)
133+
trap -- '' ${signalsToIgnore.join(" ")}
134+
135+
echo "started"
136+
# Infinite loop
137+
while true
138+
do
139+
sleep 0.010
140+
done
141+
"""
99142
return fakeFrozenUnity
100143
}
101144
}

src/main/groovy/wooga/gradle/unity/tasks/ClearDanglingUnityFiles.groovy

+31-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import org.gradle.api.tasks.InputDirectory
1313
import org.gradle.api.tasks.Optional
1414
import org.gradle.api.tasks.TaskAction
1515

16+
import java.util.concurrent.TimeUnit
17+
import java.util.concurrent.TimeoutException
18+
1619
public class ClearDanglingUnityFiles extends DefaultTask implements BaseSpec {
1720

1821
@InputDirectory
@@ -52,27 +55,49 @@ public class ClearDanglingUnityFiles extends DefaultTask implements BaseSpec {
5255
def terminateProcess = terminateOpenProcess.getOrElse(false)
5356

5457
def maybeProjectProcess = findOpenUnityProcessForProject(projectDir)
55-
if (terminateProcess && maybeProjectProcess.isPresent()) {
56-
def terminated = destroyProcess(maybeProjectProcess.get())
57-
if (!terminated) {
58-
logger.warn("Failed to terminate Unity process with PID ${it.pid()}")
58+
maybeProjectProcess.ifPresent {projectProcess ->
59+
if (terminateProcess) {
60+
def terminated = destroyProcess(projectProcess)
61+
if (!terminated) {
62+
logger.warn("Failed to terminate Unity process with PID ${projectProcess.pid()}")
63+
}
64+
} else {
65+
logger.info("Found Unity process with PID ${projectProcess.pid()}, but not terminating as terminateOpenProcess=false. " +
66+
"Also won't delete Temp folder.")
5967
}
6068
}
6169
if(maybeProjectProcess.empty || terminateProcess) {
6270
def tempFolder = new File(projectDir, "Temp")
71+
logger.info("Deleting the ${tempFolder.absolutePath} folder.")
6372
tempFolder.deleteDir()
6473
}
6574
}
6675

6776

68-
static boolean destroyProcess(ProcessHandle process) {
69-
def terminated = process.destroy()
77+
boolean destroyProcess(ProcessHandle process) {
78+
logger.info("Terminating Unity process with PID ${process.pid()} (SIGTERM).")
79+
process.destroy()
80+
logger.info("Waiting up to 5s for the process to terminate gracefully.")
81+
def terminated = waitForTermination(process, 5, TimeUnit.SECONDS)
7082
if (!terminated) {
83+
logger.info("Unity process with PID ${process.pid()} is still alive, trying to forcibly terminate it (SIGKILL)")
7184
return process.destroyForcibly()
7285
}
86+
logger.info("Process with PID ${process.pid()} terminated gracefully.")
7387
return terminated
7488
}
7589

90+
static boolean waitForTermination(ProcessHandle process, long timeout, TimeUnit timeUnit) {
91+
try {
92+
process.onExit().get(timeout, timeUnit)
93+
return true
94+
} catch (TimeoutException e) {
95+
return false
96+
} catch (Exception e) {
97+
throw e
98+
}
99+
}
100+
76101
java.util.Optional<ProcessHandle> findOpenUnityProcessForProject(File projectDir) {
77102
def editorInstanceFile = new File(projectDir, "Library/EditorInstance.json")
78103
if (!editorInstanceFile.exists()) {

0 commit comments

Comments
 (0)