production-taskbar-client / src / main / workers / winEventHook.js
winEventHook.js
Raw
/* 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()}`
);