/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2023 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/dce.h"
#include "libc/limits.h"
#include "libc/macros.h"
#include "libc/mem/mem.h"
#include "libc/nt/enum/formatmessageflags.h"
#include "libc/nt/enum/lang.h"
#include "libc/nt/enum/processaccess.h"
#include "libc/nt/process.h"
#include "libc/nt/runtime.h"
#include "libc/runtime/runtime.h"
#include "libc/stdio/internal.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/sig.h"
#include "libc/wctype.h"
#include "libc/x/x.h"
#include "third_party/getopt/getopt.internal.h"

/**
 * @fileoverview Mass Process Killer for Windows.
 *
 * Bad things sometimes happen during development, such as fork bombs.
 * Under such circumstances, the Windows operating system itself remains
 * remarkably stable (much more so than Linux would in these cases) but
 * the tools on Windows for managing processes do not scale gracefully;
 * GUIs like the Task Manager and Process Explorer become unresponsive
 * when the system has a nontrivial number of processes, leaving no way
 * to kill the processes. This tool is designed to make it easy to kill
 * processes, particularly in large numbers, via a simple CLI interface.
 */

static const char *prog;
static char16_t **filters;
static uint32_t pids[10000];

static wontreturn void PrintUsage(int rc, FILE *f) {
  fprintf(f,
          "Usage: %s [-nshv] NAME...\n"
          "\tNAME\tcase-insensitive process name substring filter\n"
          "\t-n\tdry run (print matching processes but do not kill)\n"
          "\t-v\tverbose mode\n"
          "\t-s\tsilent mode\n"
          "\t-h\tshow help\n",
          prog);
  exit(rc);
}

static wontreturn void OutOfMemory(void) {
  fprintf(stderr, "%s: out of memory\n", prog);
  exit(1);
}

static void *Calloc(size_t n, size_t z) {
  void *p;
  if (!(p = calloc(n, z)))
    OutOfMemory();
  return p;
}

static void ConvertStringToLowercase16(char16_t *s) {
  while (*s) {
    *s = towlower(*s);
    ++s;
  }
}

static bool ShouldKillProcess(char16_t *name) {
  if (!*filters) {
    return true;
  }
  for (char16_t **f = filters; *f; ++f) {
    if (strstr16(name, *f)) {
      return true;
    }
  }
  return false;
}

static int64_t MyOpenProcess(uint32_t pid) {
  return OpenProcess(
      kNtProcessTerminate | kNtProcessQueryInformation | kNtProcessVmRead,
      false, pid);
}

static bool GetProcessName(int64_t hand, char16_t name[hasatleast PATH_MAX]) {
  int64_t hMod;
  uint32_t cbNeeded;
  *name = 0;
  if (EnumProcessModules(hand, &hMod, sizeof(hMod), &cbNeeded)) {
    GetModuleBaseName(hand, hMod, name, PATH_MAX);
  }
  return !!name[0];
}

static char16_t *DescribeError(int err) {
  static char16_t msg[256];
  FormatMessage(kNtFormatMessageFromSystem | kNtFormatMessageIgnoreInserts, 0,
                err, MAKELANGID(kNtLangNeutral, kNtSublangDefault), msg,
                ARRAYLEN(msg), 0);
  return chomp16(msg);
}

int main(int argc, char *argv[]) {
  int i, j;

  // get program name
  prog = argv[0] ? argv[0] : "killall";

  // sanity check environment
  if (!IsWindows()) {
    fprintf(stderr, "%s: this program is intended for windows\n", prog);
    exit(1);
  }

  // try to minimize terminal writes slowing us down
  setvbuf(stdin, NULL, _IONBF, 0);

  // get flags
  int opt;
  bool dryrun = false;
  bool silent = false;
  bool verbose = false;
  while ((opt = getopt(argc, argv, "nhsv")) != -1) {
    switch (opt) {
      case 'n':
        dryrun = true;
        break;
      case 's':
        silent = true;
        break;
      case 'v':
        verbose = true;
        break;
      case 'h':
        PrintUsage(0, stdout);
      default:
        PrintUsage(1, stderr);
    }
  }

  // get names of things to kill
  filters = Calloc(argc, sizeof(char16_t *));
  for (j = 0, i = optind; i < argc; ++i) {
    char16_t *filter;
    if ((filter = utf8to16(argv[i], -1, 0)) && *filter) {
      ConvertStringToLowercase16(filter);
      filters[j++] = filter;
    }
  }
  if (!j && !dryrun) {
    fprintf(stderr, "%s: missing operand\n", prog);
    exit(1);
  }

  // outer loop
  int count = 0;
  int subcount;
  do {

    // get pids of all processes on system
    uint32_t n;
    if (!EnumProcesses(pids, sizeof(pids), &n)) {
      fprintf(stderr, "%s: EnumProcesses() failed: %hs\n", prog,
              DescribeError(GetLastError()));
      exit(1);
    }
    n /= sizeof(uint32_t);

    // kill matching processes
    int64_t hProcess;
    char16_t name[PATH_MAX];
    for (subcount = i = 0; i < n; i++) {
      if (!pids[i])
        continue;
      if ((hProcess = MyOpenProcess(pids[i]))) {
        if (GetProcessName(hProcess, name)) {
          ConvertStringToLowercase16(name);
          if (ShouldKillProcess(name)) {
            if (dryrun) {
              if (!silent) {
                printf("%5u %hs\n", pids[i], name);
              }
              ++subcount;
            } else if (TerminateProcess(hProcess, SIGKILL)) {
              if (!silent) {
                printf("%5u %hs killed\n", pids[i], name);
              }
              ++subcount;
            } else {
              printf("%5u %hs %hs\n", pids[i], name,
                     DescribeError(GetLastError()));
            }
          }
        } else if (verbose) {
          fprintf(stderr, "%s: GetProcessName(%u) failed: %hs\n", prog, pids[i],
                  DescribeError(GetLastError()));
        }
        CloseHandle(hProcess);
      } else if (verbose) {
        fprintf(stderr, "%s: OpenProcess(%u) failed: %hs\n", prog, pids[i],
                DescribeError(GetLastError()));
      }
    }

    // we don't stop until we've confirmed they're all gone
    count += subcount;
  } while (!dryrun && subcount);

  if (!silent && !count) {
    fprintf(stderr, "%s: no processes found\n", prog);
  }

  return 0;
}