/* $Id: kill.c 3352 2020-06-05 00:31:50Z bird $ */ /** @file * kmk kill - Process killer. */ /* * Copyright (c) 2007-2020 knut st. osmundsen * * 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 * */ /******************************************************************************* * Header Files * *******************************************************************************/ #include #include #include #include #ifdef WINDOWS32 # include # include # include #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] [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