Skip to content

[JENKINS-75563] First draft on how to kill a windows container #1724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</scm>

<properties>
<changelist>999999-SNAPSHOT</changelist>
<changelist>999999876754234456445543ewfew-SNAPSHOT</changelist>
<jenkins.host.address />
<slaveAgentPort />
<jenkins.baseline>2.504</jenkins.baseline>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
import io.fabric8.kubernetes.client.dsl.ExecWatch;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
Expand Down Expand Up @@ -264,6 +266,7 @@ public void setNodeContext(KubernetesNodeContext nodeContext) {
this.nodeContext = nodeContext;
}

private FilePath workspace;
@Override
public Launcher decorate(final Launcher launcher, final Node node) {

Expand All @@ -280,6 +283,7 @@ public Proc launch(ProcStarter starter) throws IOException {
// find container working dir
KubernetesSlave slave = (KubernetesSlave) node;
FilePath containerWorkingDirFilePath = starter.pwd();
workspace = containerWorkingDirFilePath;
String containerWorkingDirFilePathStr = containerWorkingDirFilePath != null
? containerWorkingDirFilePath.getRemote()
: ContainerTemplate.DEFAULT_WORKING_DIR;
Expand Down Expand Up @@ -662,20 +666,38 @@ public void kill(Map<String, String> modelEnvVars) throws IOException, Interrupt
getListener().getLogger().println("Killing processes");

String cookie = modelEnvVars.get(COOKIE_VAR);

int exitCode = doLaunch(
true,
int exitCode = 1;
if (this.isUnix()) {
exitCode = doLaunch(
true,
null,
null,
null,
null,
"sh",
"-c",
"kill \\`grep -l '" + COOKIE_VAR + "=" + cookie
+ "' /proc/*/environ | cut -d / -f 3 \\`")
.join();
} else {
try {
String remote = copyKillScript(workspace, "kill-processes-with-cookie", ".ps1").getRemote();
String csCode = copyKillScript(workspace, "ProcessEnvironmentReader", ".cs").getRemote();
exitCode = doLaunch( // Will fail if the script is not present, but it was also failing before in all cases
false,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For development, may move to true or introduce a system property in the future

null,
null,
null,
null,
// TODO Windows
"sh",
"-c",
"kill \\`grep -l '" + COOKIE_VAR + "=" + cookie
+ "' /proc/*/environ | cut -d / -f 3 \\`")
.join();

"powershell.exe", "-NoProfile", "-File",
/* path to file may contain spaces so wrap in double quotes*/
"\""+remote+"\"",
"-cookie", cookie, "-csFile", "\""+csCode+"\""
).join();
} catch (Exception e) {
LOGGER.log(Level.FINE, "Exception killing processes", e);
}
}
getListener().getLogger().println("kill finished with exit code " + exitCode);
}

Expand All @@ -691,6 +713,24 @@ private void setupEnvironmentVariable(EnvVars vars, PrintStream out, boolean win
}
}
}

private FilePath copyKillScript(FilePath workspace, String scriptName, String scriptSuffix) throws IOException, InterruptedException {
InputStream resourceStream = ContainerExecDecorator.class.getResourceAsStream("scripts/" + scriptName + scriptSuffix);
if (resourceStream == null) {
throw new FileNotFoundException("Script not found in resources!");
}
FilePath tempFile = workspace.createTempFile(scriptName,scriptSuffix);
try (OutputStream os = tempFile.write()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = resourceStream.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
} finally {
resourceStream.close();
}
return tempFile;
}
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Text;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;

public class ProcessEnvironmentReader
{
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_BASIC_INFORMATION
{
public IntPtr Reserved1;
public IntPtr PebBaseAddress;
public IntPtr Reserved2_0;
public IntPtr Reserved2_1;
public IntPtr UniqueProcessId;
public IntPtr Reserved3;
}

[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(
IntPtr ProcessHandle,
int ProcessInformationClass,
ref PROCESS_BASIC_INFORMATION ProcessInformation,
uint ProcessInformationLength,
out uint ReturnLength);

[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(
uint dwDesiredAccess,
bool bInheritHandle,
int dwProcessId);

[DllImport("kernel32.dll")]
private static extern bool ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesRead);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hHandle);

private const uint PROCESS_QUERY_INFORMATION = 0x0400;
private const uint PROCESS_VM_READ = 0x0010;
private const int ProcessBasicInformation = 0;

public static string ReadEnvironmentBlock(int pid)
{
IntPtr hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
if (hProcess == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error());

try
{
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
uint tmp;
int status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, ref pbi, (uint)Marshal.SizeOf(pbi), out tmp);
if (status != 0)
throw new Win32Exception("NtQueryInformationProcess failed");

// Offsets for Environment variables are different on 32/64 bit
// The following offsets are for Windows x64 - for x86 some offsets would need adjusting!
// PEB is at pbi.PebBaseAddress
// In PEB, offset 0x20 (Win10 x64, might differ!) is ProcessParameters
byte[] procParamsPtr = new byte[IntPtr.Size];
IntPtr bytesRead;
IntPtr processParametersAddr;

// Offset to ProcessParameters
int offsetProcessParameters = 0x20;
if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress + offsetProcessParameters, procParamsPtr, procParamsPtr.Length, out bytesRead))
throw new Win32Exception("ReadProcessMemory (ProcessParameters) failed");

processParametersAddr = (IntPtr)BitConverter.ToInt64(procParamsPtr, 0);

// Offset in RTL_USER_PROCESS_PARAMETERS for Environment = 0x80 (x64)!
int offsetEnvironment = 0x80;
byte[] environmentPtr = new byte[IntPtr.Size];

if (!ReadProcessMemory(hProcess, processParametersAddr + offsetEnvironment, environmentPtr, environmentPtr.Length, out bytesRead))
throw new Win32Exception("ReadProcessMemory (Environment) failed");

IntPtr environmentAddr = (IntPtr)BitConverter.ToInt64(environmentPtr, 0);

// Read an arbitrary chunk (say, 32 KB) where env block should fit
int envSize = 0x8000;
byte[] envData = new byte[envSize];
if (!ReadProcessMemory(hProcess, environmentAddr, envData, envData.Length, out bytesRead))
throw new Win32Exception("ReadProcessMemory (Environment data) failed");

// Environment block is Unicode, ends with two 0 chars.
string env = Encoding.Unicode.GetString(envData);
int end = env.IndexOf("\0\0");

if (end > -1)
env = env.Substring(0, end);

return env.Replace('\0', '\n');
}
finally
{
CloseHandle(hProcess);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
param(
[string]$cookie,
[string]$csFile
)

Add-Type -Path $csFile

$failed = $false
Get-Process | ForEach-Object {
$id = $_.Id
try {
$envBlock = [ProcessEnvironmentReader]::ReadEnvironmentBlock($id)
if ($envBlock.Contains("JENKINS_SERVER_COOKIE=$($cookie)")) {
Write-Host "Killing $($_.ProcessName) (ID: $($_.Id)) - JENKINS_SERVER_COOKIE matches"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I remove this? It is not gonna be visible to the end user but may help on local runsa nd debugging

try {
Stop-Process -Id $id -Force
Write-Host "Killed."
} catch {
Write-Host "Failed to kill process: $id"
$failed = $true
}
}
} catch {
Write-Error "Failed to read environment variables for $id"
}

}
if ($failed) {
exit 1
} else {
exit 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -746,14 +746,12 @@ public void windowsContainer() throws Exception {
r.assertLogContains("C:\\Users\\ContainerAdministrator", b);
r.assertLogContains("got stuff: some value", b);
}

@Ignore(
"TODO aborts, but with “kill finished with exit code 9009” and “After 20s process did not stop” and no graceful shutdown")

@Test
public void interruptedPodWindows() throws Exception {
assumeWindows(WINDOWS_1809_BUILD);
cloud.setDirectConnection(false);
r.waitForMessage("starting to sleep", b);
r.waitForMessage("starting ping", b);
b.getExecutor().interrupt();
r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b));
r.assertLogContains("shut down gracefully", b);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ spec:
''') {
node(POD_LABEL) {
container('shell') {
powershell 'try {Write-Host starting to sleep; Start-Sleep 999999} finally {Write-Host shut down gracefully}'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would only work if a user sends a Ctrl-C to the process, Stop-Process is a hard kill that inmediately kills the process. I have not found a way to do a "soft kill" in windows via powershell and I honestly do not believe is needed.

Copy link
Member

@jglick jglick Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it would certainly be better for the plugin to emulate the Ctrl-C-like behavior, providing functional parity with the Linux version. If this is simply impossible, then a hard kill is I guess better than the nothing we have now.

What happens if the pod is terminated? (gracefully, e.g. kubectl delete pod) Does that run any Powershell finally code? Tricky to test since you would have to look for some effect outside the pod itself.

Copy link
Author

@raul-arabaolaza raul-arabaolaza Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought something similar yesterday while trying to sleep, I have found some examples of C# code that seems to emulate the ctrl+c that I want to try. I will also try to test the pod deletion.

try {
bat 'echo "starting ping" && ping 127.0.0.1 -n 3601 > test.txt'
} catch (Exception e) { //If killing works we should be able to do things with test.txt
bat 'rename test.txt test2.txt && echo "shut down gracefully"'
}
}
}
}