summaryrefslogtreecommitdiffstats
path: root/src/kmk/kmkbuiltin/kill.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/kmk/kmkbuiltin/kill.c')
-rw-r--r--src/kmk/kmkbuiltin/kill.c653
1 files changed, 653 insertions, 0 deletions
diff --git a/src/kmk/kmkbuiltin/kill.c b/src/kmk/kmkbuiltin/kill.c
new file mode 100644
index 0000000..7ba6f17
--- /dev/null
+++ b/src/kmk/kmkbuiltin/kill.c
@@ -0,0 +1,653 @@
+/* $Id: kill.c 3352 2020-06-05 00:31:50Z bird $ */
+/** @file
+ * kmk kill - Process killer.
+ */
+
+/*
+ * Copyright (c) 2007-2020 knut st. osmundsen <bird-kBuild-spamx@anduin.net>
+ *
+ * This file is part of kBuild.
+ *
+ * kBuild is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * kBuild is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with kBuild. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+/*******************************************************************************
+* Header Files *
+*******************************************************************************/
+#include <assert.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <string.h>
+#ifdef WINDOWS32
+# include <process.h>
+# include <Windows.h>
+# include <tlhelp32.h>
+#endif
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+#include "err.h"
+#include "kmkbuiltin.h"
+
+
+/*********************************************************************************************************************************
+* Defined Constants And Macros *
+*********************************************************************************************************************************/
+/** @name kmkKillMatchName flags
+ * @{ */
+#define KMKKILL_FN_EXE_SUFF 1
+#define KMKKILL_FN_WILDCARD 2
+#define KMKKILL_FN_WITH_PATH 4
+#define KMKKILL_FN_ROOT_SLASH 8
+/** @} */
+
+
+/*********************************************************************************************************************************
+* Structures and Typedefs *
+*********************************************************************************************************************************/
+typedef struct KMKKILLGLOBALS
+{
+ PKMKBUILTINCTX pCtx;
+ int cVerbosity;
+ const char *pszCur;
+} KMKKILLGLOBALS;
+
+
+
+/**
+ * Kill one process by it's PID.
+ *
+ * The name is optional and only for messaging.
+ */
+static int kmkKillProcessByPid(KMKKILLGLOBALS *pThis, pid_t pid, const char *pszName)
+{
+ int rcExit;
+ HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE /*bInherit*/, pid);
+ if (!hProcess)
+ rcExit = errx(pThis->pCtx, 1, "Failed to open pid %u (%#x%s%s): %u",
+ pid, pid, pszName ? ", " : "", pszName ? pszName : "", GetLastError());
+ else
+ {
+ if (!TerminateProcess(hProcess, DBG_TERMINATE_PROCESS))
+ rcExit = errx(pThis->pCtx, 1, "TerminateProcess failed on pid %u (%#x%s%s): %u",
+ pid, pid, pszName ? ", " : "", pszName ? pszName : "", GetLastError());
+ else if (pThis->cVerbosity > 0)
+ kmk_builtin_ctx_printf(pThis->pCtx, 0, "Terminated %u (%#x)%s%s\n",
+ pid, pid, pszName ? " - " : "", pszName ? pszName : "");
+ CloseHandle(hProcess);
+ }
+ return rcExit;
+}
+
+
+/**
+ * Does simple wilcard filename matching.
+ *
+ * @returns 1 on match, 0 on mismatch.
+ * @param pszPattern The pattern.
+ * @param cchPattern The length of the pattern.
+ * @param pchFilename The filename to match. This does not have to be
+ * terminated at @a cchFilename.
+ * @param cchFilename The length of the filename that we should match.
+ * @param cDepth The recursion depth.
+ */
+static int kmkKillMatchWildcard(const char *pszPattern, size_t cchPattern,
+ const char *pchFilename, size_t cchFilename, unsigned cDepth)
+{
+ while (cchPattern > 0 && cchFilename > 0)
+ {
+ char chPat = *pszPattern++;
+ cchPattern--;
+ if (chPat != '*')
+ {
+ if (chPat != '?')
+ {
+ char chExe = *pchFilename;
+ if ( chExe != chPat
+ && tolower(chExe) != tolower(chPat))
+ return 0;
+ }
+ pchFilename++;
+ cchFilename--;
+ }
+ else
+ {
+ size_t off, cchExeMin;
+
+ while (cchPattern > 0 && *pszPattern == '*')
+ {
+ pszPattern++;
+ cchPattern--;
+ }
+
+ /* Trailing '*'? */
+ if (!cchPattern)
+ return 1;
+
+ /* Final wildcard? Match the tail. */
+ if (memchr(pszPattern, '*', cchPattern) == NULL)
+ {
+ if (memchr(pszPattern, '?', cchPattern) == NULL)
+ return cchFilename >= cchPattern
+ && strnicmp(&pchFilename[cchFilename - cchPattern], pszPattern, cchPattern) == 0;
+
+ /* Only '?', so we know the exact length of this '*' match and can do a single tail matching. */
+ return cchFilename >= cchPattern
+ && kmkKillMatchWildcard(pszPattern, cchPattern, &pchFilename[cchFilename - cchPattern], cchPattern, cDepth + 1);
+ }
+
+ /*
+ * More wildcards ('*'), so we need to need to try out all matching
+ * length for this one. We start by counting non-asterisks chars
+ * in the remaining pattern so we know when to stop trying.
+ * This must be at least 1 char.
+ */
+ if (cDepth >= 32)
+ return 0;
+
+ for (off = cchExeMin = 0; off < cchPattern; off++)
+ cchExeMin += pszPattern[off] != '*';
+ assert(cchExeMin > 0);
+
+ while (cchFilename >= cchExeMin)
+ {
+ if (kmkKillMatchWildcard(pszPattern, cchPattern, pchFilename, cchFilename, cDepth + 1))
+ return 1;
+ /* next */
+ pchFilename++;
+ cchFilename--;
+ }
+ return 0;
+ }
+ }
+
+ /* If there is more filename left, we won't have a match. */
+ if (cchFilename != 0)
+ return 0;
+
+ /* If there is pattern left, we still have a match if it's all asterisks. */
+ while (cchPattern > 0 && *pszPattern == '*')
+ {
+ pszPattern++;
+ cchPattern--;
+ }
+ return cchPattern == 0;
+}
+
+
+/**
+ * Matches a process name against the given pattern.
+ *
+ * @returns 1 if it matches, 0 if it doesn't.
+ * @param pszPattern The pattern to match against.
+ * @param cchPattern The pattern length.
+ * @param fPattern Pattern characteristics.
+ * @param pszExeFile The EXE filename to match (includes path).
+ * @param cchExeFile The length of the EXE filename.
+ */
+static int kmkKillMatchName(const char *pszPattern, size_t cchPattern, unsigned fPattern,
+ const char *pszExeFile, size_t cchExeFile)
+{
+ /*
+ * Automatically match the exe suffix if not present in the pattern.
+ */
+ if ( !(fPattern & KMKKILL_FN_EXE_SUFF)
+ && cchExeFile > 4
+ && stricmp(&pszExeFile[cchExeFile - 4], ".exe") == 0)
+ cchExeFile -= sizeof(".exe") - 1;
+
+ /*
+ * If no path in the pattern, move pszExeFile up to the filename part.
+ */
+ if (!(fPattern & KMKKILL_FN_WITH_PATH)
+ && ( memchr(pszExeFile, '\\', cchExeFile) != NULL
+ || memchr(pszExeFile, '/', cchExeFile) != NULL
+ || memchr(pszExeFile, ':', cchExeFile) != NULL))
+ {
+ size_t offFilename = cchExeFile;
+ char ch;
+ while ( offFilename > 0
+ && (ch = pszExeFile[offFilename - 1]) != '\\'
+ && ch != '//'
+ && ch != ':')
+ offFilename--;
+ cchExeFile -= offFilename;
+ pszExeFile += offFilename;
+ }
+
+ /*
+ * Wildcard? This only works for the filename part.
+ */
+ if (fPattern & KMKKILL_FN_WILDCARD)
+ return kmkKillMatchWildcard(pszPattern, cchPattern, pszExeFile, cchExeFile, 0);
+
+ /*
+ * No-wildcard pattern.
+ */
+ if (cchExeFile >= cchPattern)
+ {
+ if (strnicmp(&pszExeFile[cchExeFile - cchPattern], pszPattern, cchPattern) == 0)
+ return cchExeFile == cchPattern
+ || (fPattern & KMKKILL_FN_ROOT_SLASH)
+ || pszExeFile[cchExeFile - cchPattern - 1] == '\\'
+ || pszExeFile[cchExeFile - cchPattern - 1] == '/'
+ || pszExeFile[cchExeFile - cchPattern - 1] == ':';
+
+ /*
+ * Might be slash directions or multiple consequtive slashes
+ * making a difference here, so compare char-by-char from the end.
+ */
+ if (fPattern & KMKKILL_FN_WITH_PATH)
+ {
+ while (cchPattern > 0 && cchExeFile > 0)
+ {
+ char chPat = pszPattern[--cchPattern];
+ char chExe = pszExeFile[--cchExeFile];
+ if (chPat == chExe)
+ {
+ if (chPat != '\\' && chPat != '/')
+ {
+ if (chPat != ':' || cchPattern > 0)
+ continue;
+ return 1;
+ }
+ }
+ else
+ {
+ chPat = tolower(chPat);
+ chExe = tolower(chExe);
+ if (chPat == chExe)
+ continue;
+ if (chPat == '/')
+ chPat = '\\';
+ if (chExe == '/')
+ chExe = '\\';
+ if (chPat != chExe)
+ return 0;
+ }
+
+ while (cchPattern > 0 && ((chPat = pszPattern[cchPattern - 1] == '\\') || chPat == '/'))
+ cchPattern--;
+ if (!cchPattern)
+ return 1;
+
+ while (cchExeFile > 0 && ((chExe = pszExeFile[cchExeFile - 1] == '\\') || chExe == '/'))
+ cchExeFile--;
+ }
+
+ if ( cchExeFile == 0
+ || pszExeFile[cchExeFile - 1] == '\\'
+ || pszExeFile[cchExeFile - 1] == '/'
+ || pszExeFile[cchExeFile - 1] == ':')
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+/**
+ * Analyzes pattern for kmkKillMatchName().
+ *
+ * @returns Pattern characteristics.
+ * @param pszPattern The pattern.
+ * @param cchPattern The pattern length.
+ */
+static unsigned kmkKillAnalyzePattern(const char *pszPattern, size_t cchPattern)
+{
+ unsigned fPattern = 0;
+ if (cchPattern > 4 && stricmp(&pszPattern[cchPattern - 4], ".exe") == 0)
+ fPattern |= KMKKILL_FN_EXE_SUFF;
+ if (memchr(pszPattern, '*', cchPattern) != NULL)
+ fPattern |= KMKKILL_FN_WILDCARD;
+ if (memchr(pszPattern, '?', cchPattern) != NULL)
+ fPattern |= KMKKILL_FN_WILDCARD;
+ if (memchr(pszPattern, '/', cchPattern) != NULL)
+ fPattern |= KMKKILL_FN_WITH_PATH;
+ if (memchr(pszPattern, '\\', cchPattern) != NULL)
+ fPattern |= KMKKILL_FN_WITH_PATH;
+ if (*pszPattern == '\\' || *pszPattern == '/')
+ fPattern |= KMKKILL_FN_ROOT_SLASH;
+ return fPattern;
+}
+
+
+/**
+ * Enumerate processes and kill the ones matching the pattern.
+ */
+static int kmkKillProcessByName(KMKKILLGLOBALS *pThis, const char *pszPattern)
+{
+ HANDLE hSnapshot;
+ int rcExit = 0;
+ size_t const cchPattern = strlen(pszPattern);
+ unsigned const fPattern = kmkKillAnalyzePattern(pszPattern, cchPattern);
+ if (cchPattern == 0)
+ return errx(pThis->pCtx, 1, "Empty process name!");
+ if ((fPattern & (KMKKILL_FN_WILDCARD | KMKKILL_FN_WITH_PATH)) == (KMKKILL_FN_WILDCARD | KMKKILL_FN_WITH_PATH))
+ return errx(pThis->pCtx, 1, "Wildcard and paths cannot be mixed: %s", pszPattern);
+
+ hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (!hSnapshot)
+ rcExit = errx(pThis->pCtx, 1, "CreateToolhelp32Snapshot failed: %d", GetLastError());
+ else
+ {
+ union
+ {
+ PROCESSENTRY32 Entry;
+ char achBuf[8192];
+ } u;
+
+ memset(&u, 0, sizeof(u));
+ u.Entry.dwSize = sizeof(u) - sizeof(".exe");
+ SetLastError(NO_ERROR);
+ if (Process32First(hSnapshot, &u.Entry))
+ {
+ for (;;)
+ {
+ /* Kill it if it matches. */
+ u.achBuf[sizeof(u.achBuf) - sizeof(".exe")] = '\0';
+ if ( u.Entry.szExeFile[0] != '\0'
+ && kmkKillMatchName(pszPattern, cchPattern, fPattern, u.Entry.szExeFile, strlen(u.Entry.szExeFile)))
+ {
+ int rcExit2 = kmkKillProcessByPid(pThis, u.Entry.th32ProcessID, u.Entry.szExeFile);
+ if (rcExit2 != 0 && rcExit == 0)
+ rcExit = rcExit2;
+ }
+
+ /* next */
+ u.Entry.dwSize = sizeof(u) - sizeof(".exe");
+ SetLastError(NO_ERROR);
+ if (!Process32Next(hSnapshot, &u.Entry))
+ {
+ if (GetLastError() != ERROR_NO_MORE_FILES)
+ rcExit = errx(pThis->pCtx, 1, "Process32Next failed: %d", GetLastError());
+ break;
+ }
+ }
+ }
+ else
+ rcExit = errx(pThis->pCtx, 1, "Process32First failed: %d", GetLastError());
+ CloseHandle(hSnapshot);
+ }
+ return rcExit;
+
+}
+
+
+/**
+ * Worker for handling one command line process target.
+ *
+ * @returns 0 on success, non-zero exit code on failure.
+ */
+static int kmkKillProcess(KMKKILLGLOBALS *pThis, const char *pszTarget)
+{
+ /*
+ * Try treat the target as a pid first, then a name pattern.
+ */
+ char *pszNext = NULL;
+ long pid;
+ errno = 0;
+ pid = strtol(pszTarget, &pszNext, 0);
+ if (pszNext && *pszNext == '\0' && errno == 0)
+ return kmkKillProcessByPid(pThis, pid, NULL);
+ return kmkKillProcessByName(pThis, pszTarget);
+}
+
+
+/**
+ * Worker for handling one command line job target.
+ *
+ * @returns 0 on success, non-zero exit code on failure.
+ */
+static int kmkKillJob(KMKKILLGLOBALS *pThis, const char *pszTarget)
+{
+ int rcExit = 0;
+ HANDLE hTempJob = NULL;
+ BOOL fIsInJob = FALSE;
+
+ /*
+ * Open the job object.
+ */
+ DWORD fRights = JOB_OBJECT_QUERY | JOB_OBJECT_TERMINATE | JOB_OBJECT_ASSIGN_PROCESS;
+ HANDLE hJob = OpenJobObjectA(fRights, FALSE /*bInheritHandle*/, pszTarget);
+ if (!hJob)
+ {
+ fRights &= ~JOB_OBJECT_ASSIGN_PROCESS;
+ hJob = OpenJobObjectA(fRights, FALSE /*bInheritHandle*/, pszTarget);
+ if (!hJob)
+ return errx(pThis->pCtx, 1, "Failed to open job '%s': %u", pszTarget, GetLastError());
+ }
+
+ /*
+ * Are we a member of this job? If so, temporarily move
+ * to a different object so we don't kill ourselves.
+ */
+ if (IsProcessInJob(hJob, GetCurrentProcess(), &fIsInJob))
+ {
+ if (fIsInJob)
+ {
+ /** @todo this probably isn't working. */
+ if (pThis->cVerbosity >= 1)
+ kmk_builtin_ctx_printf(pThis->pCtx, 0,
+ "kmk_kill: Is myself (%u) a member of target job (%s)", getpid(), pszTarget);
+ if (!(fRights & JOB_OBJECT_ASSIGN_PROCESS))
+ rcExit = errx(pThis->pCtx, 1,
+ "Is myself member of the target job and don't have the JOB_OBJECT_ASSIGN_PROCESS right.");
+ else
+ {
+ hTempJob = CreateJobObjectA(NULL, NULL);
+ if (hTempJob)
+ {
+ if (!(AssignProcessToJobObject(hTempJob, GetCurrentProcess())))
+ rcExit = errx(pThis->pCtx, 1, "AssignProcessToJobObject(temp, me) failed: %u", GetLastError());
+ }
+ }
+ }
+ }
+ else
+ rcExit = errx(pThis->pCtx, 1, "IsProcessInJob(%s, me) failed: %u", pszTarget, GetLastError());
+
+ /*
+ * Do the killing.
+ */
+ if (rcExit == 0)
+ {
+ if (!TerminateJobObject(hJob, DBG_TERMINATE_PROCESS))
+ rcExit = errx(pThis->pCtx, 1, "TerminateJobObject(%s) failed: %u", pszTarget, GetLastError());
+ }
+
+ /*
+ * Cleanup.
+ */
+ if (hTempJob)
+ {
+ if (!(AssignProcessToJobObject(hJob, GetCurrentProcess())))
+ rcExit = errx(pThis->pCtx, 1, "AssignProcessToJobObject(%s, me) failed: %u", pszTarget, GetLastError());
+ CloseHandle(hTempJob);
+ }
+ CloseHandle(hJob);
+
+ return rcExit;
+}
+
+
+/**
+ * Show usage.
+ */
+static void kmkKillUsage(PKMKBUILTINCTX pCtx, int fIsErr)
+{
+ kmk_builtin_ctx_printf(pCtx, fIsErr,
+ "usage: %s [options] <job-name|process-name|pid> [options] [...]\n"
+ " or: %s --help\n"
+ " or: %s --version\n"
+ "\n"
+ "Options that decide what to kill next:\n"
+ " -p|--process Processes, either by name or by PID. (default)\n"
+ " -j|--job Windows jobs.\n"
+ "\n"
+ "Other options:\n"
+ " -q|--quiet Quiet operation. Only real errors are displayed.\n"
+ " -v|--verbose Increase verbosity.\n"
+ ,
+ pCtx->pszProgName, pCtx->pszProgName, pCtx->pszProgName);
+}
+
+
+int kmk_builtin_kill(int argc, char **argv, char **envp, PKMKBUILTINCTX pCtx)
+{
+ int rcExit = 0;
+ int i;
+ KMKKILLGLOBALS This;
+ enum { kTargetJobs, kTargetProcesses } enmTarget = kTargetProcesses;
+
+ /* Globals. */
+ This.pCtx = pCtx;
+ This.cVerbosity = 1;
+
+ /*
+ * Parse arguments.
+ */
+ if (argc <= 1)
+ {
+ kmkKillUsage(pCtx, 0);
+ return 1;
+ }
+ for (i = 1; i < argc; i++)
+ {
+ if (argv[i][0] == '-')
+ {
+ const char *pszValue;
+ const char *psz = &argv[i][1];
+ char chOpt;
+ chOpt = *psz++;
+ if (chOpt == '-')
+ {
+ /* Convert long to short option. */
+ if (!strcmp(psz, "job"))
+ chOpt = 'j';
+ else if (!strcmp(psz, "process"))
+ chOpt = 'p';
+ else if (!strcmp(psz, "quiet"))
+ chOpt = 'q';
+ else if (!strcmp(psz, "verbose"))
+ chOpt = 'v';
+ else if (!strcmp(psz, "help"))
+ chOpt = '?';
+ else if (!strcmp(psz, "version"))
+ chOpt = 'V';
+ else
+ {
+ errx(pCtx, 2, "Invalid argument '%s'.", argv[i]);
+ kmkKillUsage(pCtx, 1);
+ return 2;
+ }
+ psz = "";
+ }
+
+ /*
+ * Requires value?
+ */
+ switch (chOpt)
+ {
+ /*case '':
+ if (*psz)
+ pszValue = psz;
+ else if (++i < argc)
+ pszValue = argv[i];
+ else
+ return errx(pCtx, 2, "The '-%c' option takes a value.", chOpt);
+ break;*/
+
+ default:
+ pszValue = NULL;
+ break;
+ }
+
+
+ switch (chOpt)
+ {
+ /*
+ * What to kill
+ */
+ case 'j':
+ enmTarget = kTargetJobs;
+ break;
+
+ case 'p':
+ enmTarget = kTargetProcesses;
+ break;
+
+ /*
+ * How to kill processes...
+ */
+
+
+ /*
+ * Noise level.
+ */
+ case 'q':
+ This.cVerbosity = 0;
+ break;
+
+ case 'v':
+ This.cVerbosity += 1;
+ break;
+
+ /*
+ * The mandatory version & help.
+ */
+ case '?':
+ kmkKillUsage(pCtx, 0);
+ return rcExit;
+ case 'V':
+ return kbuild_version(argv[0]);
+
+ /*
+ * Invalid argument.
+ */
+ default:
+ errx(pCtx, 2, "Invalid argument '%s'.", argv[i]);
+ kmkKillUsage(pCtx, 1);
+ return 2;
+ }
+ }
+ else
+ {
+ /*
+ * Kill something.
+ */
+ int rcExitOne;
+ This.pszCur = argv[i];
+ if (enmTarget == kTargetJobs)
+ rcExitOne = kmkKillJob(&This, argv[i]);
+ else
+ rcExitOne = kmkKillProcess(&This, argv[i]);
+ if (rcExitOne != 0 && rcExit == 0)
+ rcExit = rcExitOne;
+ }
+ }
+
+ return rcExit;
+}
+
+#ifdef KMK_BUILTIN_STANDALONE
+int main(int argc, char **argv, char **envp)
+{
+ KMKBUILTINCTX Ctx = { "kmk_kill", NULL };
+ return kmk_builtin_kill(argc, argv, envp, &Ctx);
+}
+#endif
+