def _error(stream, msg):
    stream.write(f"[error] [{__file__}] {msg}\n")


def _crash_process_win(pid):
    # fmt: off
    import ctypes

    POINTER              = ctypes.POINTER
    LPVOID               = ctypes.c_void_p
    PVOID                = LPVOID
    HANDLE               = LPVOID
    PHANDLE              = POINTER(HANDLE)
    ULONG                = ctypes.c_ulong
    SIZE_T               = ctypes.c_size_t
    LONG                 = ctypes.c_long
    NTSTATUS             = LONG
    DWORD                = ctypes.c_uint32
    ACCESS_MASK          = DWORD
    INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
    BOOL                 = ctypes.c_int
    byref                = ctypes.byref
    long                 = int
    WAIT_TIMEOUT         = 0x102
    WAIT_FAILED          = 0xFFFFFFFF
    WAIT_OBJECT_0        = 0
    STANDARD_RIGHTS_ALL  = long(0x001F0000)
    SPECIFIC_RIGHTS_ALL  = long(0x0000FFFF)
    SYNCHRONIZE          = long(0x00100000)
    STANDARD_RIGHTS_REQUIRED = long(0x000F0000)
    PROCESS_ALL_ACCESS   = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF)
    THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH = long(0x00000002)

    def NT_SUCCESS(x): return x >= 0

    windll = ctypes.windll

    # HANDLE WINAPI OpenProcess(
    #   IN  DWORD dwDesiredAccess,
    #   IN  BOOL bInheritHandle,
    #   IN  DWORD dwProcessId
    #   );
    _OpenProcess = windll.kernel32.OpenProcess
    _OpenProcess.argtypes = [DWORD, BOOL, DWORD]
    _OpenProcess.restype  = HANDLE

    # NTSTATUS NtCreateThreadEx(
    #     OUT PHANDLE hThread,
    #     IN ACCESS_MASK DesiredAccess,
    #     IN PVOID ObjectAttributes,
    #     IN HANDLE ProcessHandle,
    #     IN PVOID lpStartAddress,
    #     IN PVOID lpParameter,
    #     IN ULONG Flags,
    #     IN SIZE_T StackZeroBits,
    #     IN SIZE_T SizeOfStackCommit,
    #     IN SIZE_T SizeOfStackReserve,
    #     OUT PVOID lpBytesBuffer
    #     );
    _NtCreateThreadEx = windll.ntdll.NtCreateThreadEx
    _NtCreateThreadEx.argtypes = [PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID]
    _NtCreateThreadEx.restype  = NTSTATUS

    # DWORD WINAPI WaitForSingleObject(
    #   HANDLE hHandle,
    #   DWORD dwMilliseconds
    #   );
    _WaitForSingleObject = windll.kernel32.WaitForSingleObject
    _WaitForSingleObject.argtypes = [HANDLE, DWORD]
    _WaitForSingleObject.restype  = DWORD

    hProcess = _OpenProcess(
        PROCESS_ALL_ACCESS,
        0,  # bInheritHandle
        pid
    )
    if not hProcess:
        raise ctypes.WinError()

    # this injects a new thread into the process running the test code. this thread starts executing at address 0,
    # causing a crash.
    #
    # alternatives considered:
    #
    # DebugBreakProcess(): in order for DebugBreakProcess() to send the breakpoint, a debugger must be attached.  this
    # can be accomplished with DebugActiveProcess()/WaitForDebugEvent()/ContinueDebugEvent().  unfortunately, when a
    # debugger is attached, UnhandledExceptionFilter() is ignored.  UnhandledExceptionFilter() is where the test process
    # runs the crash dump code.
    #
    # CreateRemoteThread(): this approach does not work if the target process is stuck waiting for the loader lock.
    #
    # the solution below uses NtCreateThreadEx to create the faulting thread in the test process.  unlike
    # CreateRemoteThread(), NtCreateThreadEx accepts the THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH flag which skips
    # THREAD_ATTACH in DllMain thereby avoiding the loader lock.
    hThread = HANDLE(INVALID_HANDLE_VALUE)
    status = _NtCreateThreadEx(
        byref(hThread),
        (STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL),
        0,  # ObjectAttributes
        hProcess,
        0,  # lpStartAddress (calls into null causing a crash)
        0,  # lpParameter
        THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH,
        0,  # StackZeroBits
        0,  # StackZeroBits (must be 0 to crash)
        0,  # SizeOfStackReserve
        0,  # lpBytesBuffer
    )
    if not NT_SUCCESS(status):
        raise OSError(None, "NtCreateThreadEx failed", None, status)

    waitTimeMs = 30 * 1000
    status = _WaitForSingleObject(hProcess, waitTimeMs)
    if status == WAIT_TIMEOUT:
        raise TimeoutError("timed out while waiting for target process to exit")
    elif status == WAIT_FAILED:
        raise ctypes.WinError()
    elif status != WAIT_OBJECT_0:
        raise OSError(None, "failed to wait for target process to exit", None, status)
    # fmt: on


def crash_process(process, stream):
    """
    Triggers a crash dump in the test process, terminating the process.

    Returns True if the test process was terminated, False if the process is still running.
    """
    import os

    assert process
    pid = process.pid

    if os.name == "nt":
        try:
            _crash_process_win(pid)
        except Exception as e:
            _error(stream, f"Failed crashing timed out process: {pid}. Error: {e}")
    else:
        import signal

        try:
            process.send_signal(signal.SIGABRT)
            process.wait(timeout=30)  # seconds
        except Exception as e:
            _error(stream, f"Failed crashing timed out process: {pid}. Error: {e}")

    return not process.is_running()
