/* eslint-disable no-bitwise */ import log from "electron-log"; import ffi from "@inigolabs/ffi-napi"; import os from "os"; import ref from "@inigolabs/ref-napi"; import StructType from "ref-struct-di"; import { parentPort } from "worker_threads"; import getLastErrMsg from "../utils/getLastErrMsg"; import { getWindowData } from "../utils/winapi/getWindowData"; import { BOOL, DWORD, HANDLE, HINSTANCE, HWND, LONG, LPARAM, POINTER, INT, UINT, VOID, WPARAM, } from "../utils/winapi/types"; const Struct = StructType(ref); const isWin10 = parseInt(os.release(), 10) >= 10; const is64bit = os.arch() === "x64"; log.transports.file.level = "info"; const EVENT_SYSTEM_FOREGROUND = 0x0003; const WINEVENT_OUTOFCONTEXT = 0; const WINEVENT_SKPIOWNPROCESS = 2; const DWMWA_CLOAKED = 14; const GWL_EXSTYLE = -20; const EX_WS_EX_WINDOWEDGE = 0x00000100; const EX_METRO = 2097408; const EX_WS_EX_LAYERED = 0x00080000; const EX_STYLE_WINDOWED = 0x00000110; const EX_STYLE_WINDOWED2 = 0x00050100; const TagMSG = Struct({ hwnd: HWND, // HWND message: UINT, // UINT wParam: WPARAM, // WPARAM lParam: LPARAM, // LPARAM time: DWORD, // DWORD pt: Struct({ x: LONG, y: LONG }), // POINT lPrivate: DWORD, // DWORD }); const MSG = new TagMSG(); const intPtr = ref.refType(ref.types.int); const windows = new Set(); let dwmapi; if (isWin10) { dwmapi = new ffi.Library("dwmapi", { DwmGetWindowAttribute: ["bool", [HWND, "long", "pointer", "long"]], }); } const kernel32 = ffi.Library("Kernel32", { GetCurrentThreadId: [DWORD, []], }); const user32 = ffi.Library("user32", { SetWinEventHook: [ HINSTANCE, [DWORD, DWORD, POINTER, POINTER, DWORD, DWORD, DWORD], ], GetMessageA: [INT, [POINTER, HWND, UINT, UINT]], IsWindowVisible: [BOOL, [HWND]], [is64bit ? "GetWindowLongPtrA" : "GetWindowLongA"]: ["long", [HWND, INT]], EnumWindows: [BOOL, [POINTER, LPARAM]], UnhookWinEvent: [BOOL, [HANDLE]], }); function getMessage() { return user32.GetMessageA(MSG.ref(), 0, 0, 0); } const BufferToInt = (hbuf) => { if (os.endianness() === "LE") return hbuf.readInt32LE(); return hbuf.readInt32BE(); }; function isDesktopWindow(hwnd, checkIsVisible = false) { let cloacked = false; if (isWin10) { const cloackedPtr = Buffer.alloc(intPtr.size); dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, cloackedPtr, intPtr.size); cloacked = BufferToInt(cloackedPtr); } let ex; if (is64bit) { ex = user32.GetWindowLongPtrA(hwnd, GWL_EXSTYLE); } else { ex = user32.GetWindowLongA(hwnd, GWL_EXSTYLE); } const isV = user32.IsWindowVisible(hwnd); if ( (checkIsVisible // IsWindowVisible return false on explorer open, this fix this removing check in pfnWinEventProc ? isV : true) && !cloacked && (ex === EX_WS_EX_WINDOWEDGE || ex === EX_STYLE_WINDOWED || ex === EX_STYLE_WINDOWED2 || ex === EX_METRO || ex === EX_WS_EX_LAYERED) ) { return true; } return windows.has(hwnd); // need coz destroy object hwnd is already invisible } const windowsProc = ffi.Callback(INT, [HWND, LPARAM], (hwnd) => { if (hwnd >= 0 && isDesktopWindow(hwnd, true)) { const winEvent = "enum-foreground"; const data = getWindowData(hwnd); const message = { event: winEvent, data }; windows.add(hwnd); parentPort.postMessage(message); } return 1; // continue enumerate, return true as value 1 }); user32.EnumWindows(windowsProc, 0); const pfnWinEventProc = ffi.Callback( VOID, [HWND, INT, HWND], (_hWinEventHook, _event, hwnd) => { // close check must be before foreground check, or IsWindowVisible return false sometime windows.forEach((h) => { if (user32.IsWindowVisible(h)) return; windows.delete(h); const winEvent = "closed"; const data = { hwnd: h }; const message = { event: winEvent, data }; parentPort.postMessage(message); }); if (isDesktopWindow(hwnd)) { const winEvent = "foreground"; const data = getWindowData(hwnd); const message = { event: winEvent, data }; windows.add(hwnd); parentPort.postMessage(message); } } ); // Dont used range to EVENT_OBJECT_DESTROY due massive event count and high cpu load // Single event listener much efficient but need tricks in [pfnWinEventProc] to detect closed windows const HWINEVENTHOOK = user32.SetWinEventHook( EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, null, pfnWinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKPIOWNPROCESS ); parentPort.postMessage({ event: "set-threadId", data: kernel32.GetCurrentThreadId(), }); let res = getMessage(); while (res !== 0) { if (res === -1) { log.error( `winEventHook getMessage error: ${res}, message: ${JSON.stringify( MSG.toJSON() )}, last error: ${getLastErrMsg()}` ); break; } res = getMessage(); } const result = user32.UnhookWinEvent(HWINEVENTHOOK); log.warn( `WinEventHook worker closed, unhook: ${result}, last error: ${getLastErrMsg()}` );