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

Commit

Permalink
Improve process termination on cleanDanglingUnityFiles (#205)
Browse files Browse the repository at this point in the history
## 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"
  • Loading branch information
Joaquimmnetto authored Dec 11, 2024
1 parent 327f0a8 commit 2e803b3
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,55 +47,98 @@ class ClearDanglingUnityFilesIntegrationSpec extends IntegrationSpec {
assert tempFolder.exists()
assert fakeProcess ? fakeProcess.alive : true
assert editorInstanceFile.exists() == hasEditorInstanceFile
runTasksSuccessfully("testTask")
def result = runTasksSuccessfully("testTask")

then:
println result.standardOutput
tempFolder.exists() == !shouldCleanup

cleanup:
fakeProcess?.destroy()
fakeProcess?.destroyForcibly()
editorInstanceFile.delete()


where:
hasOpenProcess | hasEditorInstanceFile | terminateProcess | shouldCleanup | prefix | suffix
false | false | false | true | "should delete" | "no running Unity process with the project [0]"
false | false | true | true | "should delete" | "no running Unity process with the project [1]"
true | true | true | true | "should delete" | "running process with the project [1]"
true | false | false | true | "should delete" | "running process with the project [2]"
true | true | false | false | "shouldnt delete" | "running process with the project"
false | false | false | true | "should delete" | "no running Unity process with the project [terminate=false]"
false | false | true | true | "should delete" | "no running Unity process with the project [terminate=true]"
true | true | true | true | "should delete" | "running process with the project [terminate=true, EditorInstance.json]"
true | false | false | true | "should delete" | "running process with the project [terminate=false, no EditorInstance.json]"
true | true | false | false | "shouldnt delete" | "running process with the project [terminate=false, EditorInstance.json]"
}

def createFakeFrozenUnityExecutable() {
@IgnoreIf({ os.windows })
def "#prefix terminate Unity process if there is a #suffix"() {
given:
def tempFolder = new File(projectDir, "Temp").with {
it.mkdir()
new File(it, "UnityLockfile").createNewFile()
return it
}
and:
def fakeUnityExec = createFakeFrozenUnityExecutable(processIgnoring)
def fakeProcess = "$fakeUnityExec.absolutePath -projectPath ${projectDir.absolutePath}".execute()
fakeProcess.waitFor(10, TimeUnit.MILLISECONDS)
File editorInstanceFile = new File(projectDir, "Library/EditorInstance.json")
editorInstanceFile.parentFile.mkdir()
editorInstanceFile << """
{
"process_id" : ${wrapValueBasedOnType(fakeProcess.pid(), Integer)},
"version" : "any",
"app_path" : "any",
"app_contents_path" : "any"
}
"""
and:
buildFile << """
tasks.register("testTask", wooga.gradle.unity.tasks.ClearDanglingUnityFiles) {
projectDirectory.set(${wrapValueBasedOnType(projectDir, File)})
terminateOpenProcess.set($terminateProcess)
}
"""

when:
assert tempFolder.exists()
assert fakeProcess ? fakeProcess.alive : true
def result = runTasksSuccessfully("testTask")

then:
println result.standardOutput
terminateProcess == !fakeProcess.alive

cleanup:
fakeProcess?.destroyForcibly()
editorInstanceFile.delete()


where:
terminateProcess | processIgnoring | prefix | suffix
false | [] | "shouldn't" | "normal running process"
false | ["SIGINT", "SIGTERM"] | "shouldn't" | "frozen process"
true | [] | "should" | "normal running process"
true | ["SIGINT", "SIGTERM"] | "should" | "frozen process"
}

def createFakeFrozenUnityExecutable(List<String> signalsToIgnore = []) {
def fakeFrozenUnity = new File(projectDir, "Unity").with {
it.createNewFile()
it.executable = true
return it
}
if (PlatformUtils.windows) {
fakeFrozenUnity <<
"""@echo off
echo 'started'
:loop
timeout /t 0.010 >nul
goto loop
"""
} else {
fakeFrozenUnity <<
"""
#!/bin/bash
# Trap the SIGINT signal (Ctrl+C) and execute a function
trap 'exit' SIGINT
echo "started"
# Infinite loop
while true
do
sleep 0.010
done
"""
}
fakeFrozenUnity <<
"""
#!/bin/bash
# Trap the SIGINT signal (Ctrl+C) and SIGTERM (kill)
trap -- '' ${signalsToIgnore.join(" ")}
echo "started"
# Infinite loop
while true
do
sleep 0.010
done
"""
return fakeFrozenUnity
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

public class ClearDanglingUnityFiles extends DefaultTask implements BaseSpec {

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

def maybeProjectProcess = findOpenUnityProcessForProject(projectDir)
if (terminateProcess && maybeProjectProcess.isPresent()) {
def terminated = destroyProcess(maybeProjectProcess.get())
if (!terminated) {
logger.warn("Failed to terminate Unity process with PID ${it.pid()}")
maybeProjectProcess.ifPresent {projectProcess ->
if (terminateProcess) {
def terminated = destroyProcess(projectProcess)
if (!terminated) {
logger.warn("Failed to terminate Unity process with PID ${projectProcess.pid()}")
}
} else {
logger.info("Found Unity process with PID ${projectProcess.pid()}, but not terminating as terminateOpenProcess=false. " +
"Also won't delete Temp folder.")
}
}
if(maybeProjectProcess.empty || terminateProcess) {
def tempFolder = new File(projectDir, "Temp")
logger.info("Deleting the ${tempFolder.absolutePath} folder.")
tempFolder.deleteDir()
}
}


static boolean destroyProcess(ProcessHandle process) {
def terminated = process.destroy()
boolean destroyProcess(ProcessHandle process) {
logger.info("Terminating Unity process with PID ${process.pid()} (SIGTERM).")
process.destroy()
logger.info("Waiting up to 5s for the process to terminate gracefully.")
def terminated = waitForTermination(process, 5, TimeUnit.SECONDS)
if (!terminated) {
logger.info("Unity process with PID ${process.pid()} is still alive, trying to forcibly terminate it (SIGKILL)")
return process.destroyForcibly()
}
logger.info("Process with PID ${process.pid()} terminated gracefully.")
return terminated
}

static boolean waitForTermination(ProcessHandle process, long timeout, TimeUnit timeUnit) {
try {
process.onExit().get(timeout, timeUnit)
return true
} catch (TimeoutException e) {
return false
} catch (Exception e) {
throw e
}
}

java.util.Optional<ProcessHandle> findOpenUnityProcessForProject(File projectDir) {
def editorInstanceFile = new File(projectDir, "Library/EditorInstance.json")
if (!editorInstanceFile.exists()) {
Expand Down

0 comments on commit 2e803b3

Please sign in to comment.