Plugins Tech Stack
Plugins Tech Stack
Warning
This document provides an overview of the native Node.js addon plugins used in the eIsland application. Plugins are Windows-only. Most are built with C and Node-API (N-API) via node-gyp, while the SMTC, Bluetooth, Power, WiFi, and Brightness Helpers use C# .NET with koffi FFI or child process integration.
Overview
The eIsland plugin system consists of five native addons that provide low-level Windows system capabilities unavailable through standard Node.js APIs:
| Plugin | Package | Purpose |
|---|---|---|
| Processes Attacker | @eisland/windows-processes-attacker | Terminate processes by name or PID |
| Fullscreen Detector | @eisland/windows-fullscreen-detector | Detect foreground fullscreen windows |
| Performance Monitor | @eisland/windows-performance-monitor | CPU, memory, and temperature snapshots |
| SMTC Helper | @eisland/windows-smtc-helper | System Media Transport Controls (play, pause, next, previous, status) |
| Bluetooth Helper | @eisland/windows-bluetooth-helper | Bluetooth device enumeration and real-time connection monitoring |
| Brightness Helper | @eisland/windows-brightness-helper | Screen brightness query, control, and real-time WMI event monitoring |
Common Characteristics:
- Language: C (core logic) + Node-API binding layer
- Build System:
node-gypwithbinding.gypconfiguration - Platform: Windows only (
"os": ["win32"]) - Architecture: x64, arm64, ia32
- License: GPL-3.0
- Module Format: Synchronous N-API exports (no async callbacks)
- TypeScript:
.d.tstype declarations provided
Build System
Node-Gyp Configuration
Each plugin uses a binding.gyp file to define compilation targets:
{
"targets": [
{
"target_name": "windows_fullscreen_detector",
"sources": ["src/fullscreen_detector.c", "src/fullscreen_core.c"],
"defines": ["WIN32_LEAN_AND_MEAN", "NAPI_VERSION=8"],
"libraries": ["user32.lib", "dwmapi.lib"]
}
]
}Build Commands:
node-gyp rebuild # Clean + configure + compile
node-gyp clean # Remove build artifactsOutput: build/Release/{module_name}.node โ a native DLL loaded by Node.js at runtime.
N-API Version
All plugins target NAPI_VERSION=8, providing:
- Stable ABI across Node.js versions (no recompilation needed on Node upgrades)
napi_create_int32,napi_create_double,napi_create_string_utf16โ typed value creationnapi_define_propertiesโ module export registrationNAPI_MODULEmacro โ automatic module initialization
Win32 Libraries Used
| Plugin | Libraries | Purpose |
|---|---|---|
| Processes Attacker | kernel32.lib | Process enumeration (CreateToolhelp32Snapshot), termination (TerminateProcess) |
| Fullscreen Detector | user32.lib, dwmapi.lib | Window enumeration (EnumWindows), DWM frame bounds (DwmGetWindowAttribute) |
| Performance Monitor | (none linked) | Uses GetSystemTimes, GlobalMemoryStatusEx from kernel32 (auto-linked) |
Plugin: Windows Processes Attacker
Package
@eisland/windows-processes-attackerPurpose
Caution
Provides the ability to terminate Windows processes by name or process ID. Used by the eIsland AI agent's win.close tool and the hide-process IPC domain. This plugin has high-risk operation permissions โ use with caution.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer โ
โ closeProcess(target) / closeProcesses(targets[]) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ N-API
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ napi_binding.c โ
โ Argument parsing: string โ name target, number โ PID targetโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ process_ops.c โ
โ CreateToolhelp32Snapshot โ Process32FirstW/NextW โ
โ โ OpenProcess โ TerminateProcess โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโExported Functions
| Function | Signature | Description |
|---|---|---|
closeProcess | (target: string | number) โ ProcessCloseResult | Terminate all processes matching a single target |
closeProcesses | (targets: (string | number)[]) โ ProcessCloseResult[] | Terminate processes for each target in an array |
Target Types
| Input Type | Matching Strategy | Example |
|---|---|---|
string | Process name (case-insensitive, auto-appends .exe) | "notepad", "chrome.exe" |
number | Process ID (exact match) | 12345 |
Result Structure
interface ProcessCloseResult {
target: string | number; // Original target
matchedCount: number; // Processes found matching target
terminatedCount: number; // Successfully terminated
failedCount: number; // Failed to terminate
failures: ProcessFailure[]; // Details of failures
}
interface ProcessFailure {
pid: number; // Process ID
name: string; // Process name
errorCode: number; // Win32 error code
}Key Implementation Details
Process Enumeration:
bool close_matching_processes(const ProcessTarget* target, ProcessCloseResult* result) {
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W entry;
entry.dwSize = sizeof(PROCESSENTRY32W);
Process32FirstW(snapshot, &entry);
do {
if (entry_matches_target(&entry, target)) {
close_process_by_entry(&entry, result);
}
} while (Process32NextW(snapshot, &entry));
CloseHandle(snapshot);
}Name Normalization:
- Automatically appends
.exeif not present - Case-insensitive comparison via
_wcsicmp - Supports both
"notepad"and"notepad.exe"as input
Termination:
- Uses
OpenProcess(PROCESS_TERMINATE, ...)+TerminateProcess(handle, 1) - Exit code
1signals termination by eIsland - Records Win32 error codes for failed terminations
Source Files
| File | Responsibility |
|---|---|
processes_attacker.c | Module entry point, N-API export registration |
napi_binding.c | Argument parsing, JS-to-C type conversion |
napi_helpers.c | JS value creation helpers (objects, arrays, strings) |
process_ops.c | Process enumeration and termination logic |
string_utils.c | Wide-string normalization and matching |
types.h | Shared type definitions (ProcessTarget, ProcessCloseResult) |
Plugin: Windows Fullscreen Detector
Package
@eisland/windows-fullscreen-detectorPurpose
Detects whether any window is currently in fullscreen mode. Used by eIsland to auto-hide the dynamic island when a user enters fullscreen (e.g., watching a video, gaming, or presenting).
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer โ
โ getForegroundFullscreenWindow() โ
โ getFullscreenWindows() โ
โ isAnyFullscreenWindow() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ N-API
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fullscreen_detector.c โ
โ N-API binding: EnumWindows callback โ JS array conversion โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fullscreen_core.c โ
โ Window filtering + monitor bounds comparison โ
โ DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโExported Functions
| Function | Signature | Description |
|---|---|---|
getForegroundFullscreenWindow | () โ FullscreenWindowInfo | null | Returns the foreground window if it is fullscreen, otherwise null |
getFullscreenWindows | () โ FullscreenWindowInfo[] | Returns all currently fullscreen windows across all monitors |
isAnyFullscreenWindow | () โ boolean | Quick boolean check โ true if the foreground window is fullscreen |
Result Structure
interface FullscreenWindowInfo {
hwnd: string; // Window handle as hex string (e.g., "0x1a2b3c")
title: string; // Window title
processId: number; // Owning process ID
bounds: NativeRect; // Window bounds (DWM-adjusted)
monitor: NativeMonitorInfo; // Monitor the window is on
isForeground: boolean; // Whether this is the foreground window
}
interface NativeRect {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
}
interface NativeMonitorInfo extends NativeRect {
isPrimary: boolean; // Whether this is the primary monitor
}Fullscreen Detection Algorithm
The detection uses a bounds-comparison approach with DWM awareness:
BOOL get_fullscreen_info(HWND hwnd, FullscreenWindowInfo* info) {
// 1. Filter candidate windows
if (!is_candidate_window(hwnd)) return FALSE;
// 2. Get DWM-adjusted window bounds
RECT bounds;
DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &bounds, sizeof(RECT));
// 3. Get monitor that owns this window
HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
GetMonitorInfoW(monitor, &monitor_info);
// 4. Compare window bounds to monitor bounds (with tolerance)
if (!is_rect_close_to_monitor(&bounds, &monitor_info.rcMonitor))
return FALSE;
// 5. Populate result
info->hwnd = hwnd;
info->bounds = bounds;
info->monitor_info = monitor_info;
GetWindowThreadProcessId(hwnd, &info->process_id);
GetWindowTextW(hwnd, info->title, TITLE_BUFFER_LENGTH);
return TRUE;
}Window Filtering Rules:
| Check | API | Purpose |
|---|---|---|
| Valid window | IsWindow(hwnd) | Skip destroyed windows |
| Visible | IsWindowVisible(hwnd) | Skip hidden windows |
| Not minimized | !IsIconic(hwnd) | Skip minimized windows |
| Not disabled | !(style & WS_DISABLED) | Skip disabled windows |
| Not tool window | !(ex_style & WS_EX_TOOLWINDOW) | Skip floating tool windows |
Bounds Comparison:
#define FULLSCREEN_TOLERANCE_PX 2
static BOOL is_rect_close_to_monitor(const RECT* window_rect, const RECT* monitor_rect) {
return abs(window_rect->left - monitor_rect->left) <= FULLSCREEN_TOLERANCE_PX &&
abs(window_rect->top - monitor_rect->top) <= FULLSCREEN_TOLERANCE_PX &&
abs(window_rect->right - monitor_rect->right) <= FULLSCREEN_TOLERANCE_PX &&
abs(window_rect->bottom - monitor_rect->bottom) <= FULLSCREEN_TOLERANCE_PX;
}A 2-pixel tolerance handles windows that report slightly off-monitor bounds (common with DWM compositing).
Multi-Monitor Support:
EnumWindowsiterates all top-level windows across all monitorsMonitorFromWindowresolves which monitor owns each window- Each result includes the monitor's
rcMonitorbounds andisPrimaryflag
Source Files
| File | Responsibility |
|---|---|
fullscreen_detector.c | N-API binding layer, JS value construction |
fullscreen_core.c | Core detection logic, window enumeration callback |
fullscreen_types.h | Shared type definitions (FullscreenWindowInfo, WindowList) |
Plugin: Windows Performance Monitor
Package
@eisland/windows-performance-monitorPurpose
Provides low-overhead system performance snapshots for CPU usage, memory utilization, and hardware temperatures. Used by the eIsland system monitoring UI and the AI agent's monitor.* tools.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js) โ
โ getCpu() / getMemory() โ native binding โ
โ getTemperature() / getHardwareList() โ .NET helper process โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ N-API โ spawnSync
โโโโโโโโโโผโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
โ performance_monitor.c โ โ eIslandTemperatureReader.exe โ
โ N-API binding โ โ .NET 10 + LibreHardware โ
โโโโโโโโโโโโโโโโโโโโโโโโโโค โ MonitorLib โ
โ performance_core.c โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ GetSystemTimes โ โ TemperatureCollector โ
โ GlobalMemoryStatusEx โ โ HardwareListCollector โ
โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโExported Functions
| Function | Signature | Description |
|---|---|---|
getCpu | () โ CpuSnapshot | CPU usage percentage (delta-based) |
getMemory | () โ MemorySnapshot | Memory usage (total, used, available) |
getTemperature | () โ TemperatureSnapshot | Hardware temperature readings via .NET helper |
getHardwareList | () โ HardwareListSnapshot | CPU and GPU device enumeration via .NET helper |
CPU Snapshot
interface CpuSnapshot {
usagePercent: number; // 0โ100, CPU usage since last call
hasBaseline: boolean; // false on first call (no prior delta)
}Implementation:
bool get_cpu_snapshot(CpuSnapshot* snapshot) {
FILETIME idle_time, kernel_time, user_time;
GetSystemTimes(&idle_time, &kernel_time, &user_time);
// Delta calculation
ULONGLONG total_delta = (kernel - last_kernel) + (user - last_user);
ULONGLONG idle_delta = idle - last_idle;
snapshot->usage_percent = clamp_percent(
(double)(total_delta - idle_delta) * 100.0 / (double)total_delta
);
}- Uses
GetSystemTimesfor kernel-level CPU time accounting - Delta-based: each call computes usage since the previous call
- First call returns
hasBaseline: false(no prior data for delta) clamp_percentensures values stay within [0, 100]
Memory Snapshot
interface MemorySnapshot {
totalBytes: number; // Total physical memory
usedBytes: number; // Total - Available
availableBytes: number; // Available physical memory
usagePercent: number; // 0โ100
}Implementation:
bool get_memory_snapshot(MemorySnapshot* snapshot) {
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
GlobalMemoryStatusEx(&status);
snapshot->total_bytes = status.ullTotalPhys;
snapshot->available_bytes = status.ullAvailPhys;
snapshot->used_bytes = status.ullTotalPhys - status.ullAvailPhys;
snapshot->usage_percent = (double)used * 100.0 / (double)total;
}- Uses
GlobalMemoryStatusExfor system-wide memory stats - Returns raw bytes (as
doubleto avoid JS number precision limits)
Temperature Snapshot
interface TemperatureSnapshot {
isAvailable: boolean; // Whether readings were obtained
readings: TemperatureReading[]; // Individual sensor readings
maxTemperatureCelsius: number | null; // Highest temperature across all sensors
}
interface TemperatureReading {
id: string; // Sensor identifier
label: string; // Human-readable label
category: 'cpu' | 'gpu' | 'motherboard' | 'storage' | 'unknown';
temperatureCelsius: number; // Temperature in ยฐC
source: 'libre-hardware-monitor'; // Data source
}Hardware List Snapshot
interface HardwareListSnapshot {
isAvailable: boolean; // Whether hardware was enumerated
cpus: HardwareDevice[]; // CPU devices
gpus: HardwareDevice[]; // GPU devices
}
interface HardwareDevice {
id: string;
name: string;
category: 'cpu' | 'gpu';
hardwareType: string; // e.g., "Cpu", "GpuNvidia", "GpuAmd"
source: 'libre-hardware-monitor';
}.NET Temperature Helper
Info
Temperature and hardware enumeration use a separate .NET 10 console application (eIslandTemperatureReader.exe) that wraps LibreHardwareMonitorLib. It operates as a CLI tool โ invoked via spawnSync, reads hardware sensors, outputs JSON to stdout, then exits.
Why a Separate Process?
- LibreHardwareMonitorLib requires COM interop and WMI access, which can destabilize a long-running process
- Cannot be safely loaded into the Electron main process (crash risk, memory leaks, handle leaks)
- Separate process provides crash isolation โ if it fails, the main app continues unaffected
spawnSyncwith a 5-second timeout ensures the main process never blocks indefinitely- Process exits after each invocation โ no lingering state or resource accumulation
Entry Point
// Program.cs
var command = args.FirstOrDefault() ?? "temperature";
var computer = new Computer
{
IsCpuEnabled = true,
IsGpuEnabled = true,
IsMotherboardEnabled = true,
IsStorageEnabled = true,
};
computer.Open();
var payload = command == "hardware-list"
? JsonSerializer.Serialize(HardwareListCollector.Collect(computer))
: JsonSerializer.Serialize(TemperatureCollector.Collect(computer));
Console.WriteLine(payload);
computer.Close();CLI Modes:
| Invocation | Mode | Output |
|---|---|---|
eIslandTemperatureReader.exe | Temperature (default) | TemperatureSnapshot JSON |
eIslandTemperatureReader.exe temperature | Temperature | TemperatureSnapshot JSON |
eIslandTemperatureReader.exe hardware-list | Hardware enumeration | HardwareListSnapshot JSON |
The Computer object from LibreHardwareMonitorLib is configured to enable all hardware categories (CPU, GPU, motherboard, storage). computer.Open() initializes WMI subscriptions and COM interfaces; computer.Close() releases them.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Program.cs โ
โ CLI entry point โ parses args, opens Computer, dispatches โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโผโโโโโโโโโโโโ โโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ TemperatureCollectorโ โ HardwareListCollector โ
โ .Collect(computer) โ โ .Collect(computer) โ
โโโโโโโโโโฌโโโโโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ HardwareCategoryMapper โ
โ Maps HardwareType โ category string โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโโโโโ
โ Snapshots.cs โ
โ Data models (POCOs) โ
โโโโโโโโโโโโโโโโโโโโโโโโTemperatureCollector
Recursively traverses the hardware tree and collects all temperature sensor readings:
static class TemperatureCollector
{
public static TemperatureSnapshot Collect(Computer computer)
{
var readings = new List<TemperatureReading>();
foreach (var hardware in computer.Hardware)
{
CollectHardwareTemperatures(hardware, readings);
}
return new TemperatureSnapshot
{
IsAvailable = readings.Count > 0,
Readings = readings,
MaxTemperatureCelsius = readings.Count == 0
? null
: readings.Max(r => r.TemperatureCelsius),
};
}
private static void CollectHardwareTemperatures(
IHardware hardware, List<TemperatureReading> readings)
{
hardware.Update(); // Refresh sensor values
// Recurse into sub-hardware (e.g., CPU cores under a CPU package)
foreach (var sub in hardware.SubHardware)
CollectHardwareTemperatures(sub, readings);
// Collect temperature sensors
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType != SensorType.Temperature || sensor.Value is null)
continue;
readings.Add(new TemperatureReading
{
Id = $"{hardware.Identifier}/{sensor.Identifier}",
Label = string.IsNullOrWhiteSpace(sensor.Name)
? hardware.Name
: $"{hardware.Name} {sensor.Name}",
Category = HardwareCategoryMapper.Map(hardware.HardwareType),
TemperatureCelsius = Math.Round(sensor.Value.Value, 1),
Source = "libre-hardware-monitor",
});
}
}
}Key behaviors:
- Recursive traversal:
hardware.SubHardwarecontains child devices (e.g., individual CPU cores, GPU memory controllers). The collector walks the full tree. hardware.Update(): Triggers a fresh read of all sensors for this hardware node. Must be called before accessingsensor.Value.- Sensor filtering: Only
SensorType.Temperaturesensors with non-null values are collected. - ID format:
{hardware.Identifier}/{sensor.Identifier}โ unique across the hardware tree, e.g.,cpu/0/temperature/2. - Label fallback: If
sensor.Nameis empty, useshardware.Namealone. - Precision: Temperature values are rounded to 1 decimal place (
Math.Round(value, 1)).
HardwareListCollector
Enumerates CPU and GPU devices from the hardware tree:
static class HardwareListCollector
{
public static HardwareListSnapshot Collect(Computer computer)
{
var cpus = new List<HardwareDevice>();
var gpus = new List<HardwareDevice>();
foreach (var hardware in computer.Hardware)
{
CollectHardwareDevice(hardware, cpus, gpus);
}
return new HardwareListSnapshot
{
IsAvailable = cpus.Count > 0 || gpus.Count > 0,
Cpus = cpus,
Gpus = gpus,
};
}
private static void CollectHardwareDevice(
IHardware hardware, List<HardwareDevice> cpus, List<HardwareDevice> gpus)
{
hardware.Update();
var category = HardwareCategoryMapper.Map(hardware.HardwareType);
if (category == "cpu")
cpus.Add(CreateHardwareDevice(hardware, category));
else if (category == "gpu")
gpus.Add(CreateHardwareDevice(hardware, category));
// Recurse into sub-hardware
foreach (var sub in hardware.SubHardware)
CollectHardwareDevice(sub, cpus, gpus);
}
private static HardwareDevice CreateHardwareDevice(
IHardware hardware, string category) => new()
{
Id = hardware.Identifier.ToString(),
Name = hardware.Name,
Category = category,
HardwareType = hardware.HardwareType.ToString(),
Source = "libre-hardware-monitor",
};
}Key behaviors:
- Category filtering: Only
"cpu"and"gpu"devices are collected. Motherboard and storage devices are excluded from the hardware list (but their temperature sensors are still collected byTemperatureCollector). HardwareTypemapping: The rawHardwareTypeenum value (e.g.,GpuNvidia,GpuAmd,GpuIntel) is preserved as a string in theHardwareTypefield for downstream differentiation.
HardwareCategoryMapper
Maps LibreHardwareMonitor's HardwareType enum to simplified category strings:
static class HardwareCategoryMapper
{
public static string Map(HardwareType hardwareType) => hardwareType switch
{
HardwareType.Cpu => "cpu",
HardwareType.GpuAmd
or HardwareType.GpuIntel
or HardwareType.GpuNvidia => "gpu",
HardwareType.Motherboard => "motherboard",
HardwareType.Storage => "storage",
_ => "unknown",
};
}Mapping Table:
HardwareType | Category | Examples |
|---|---|---|
Cpu | cpu | Intel Core, AMD Ryzen |
GpuNvidia | gpu | NVIDIA GeForce, RTX series |
GpuAmd | gpu | AMD Radeon, RX series |
GpuIntel | gpu | Intel Arc, integrated graphics |
Motherboard | motherboard | ASUS, MSI, Gigabyte boards |
Storage | storage | NVMe SSDs, SATA drives |
| (other) | unknown | SuperIO, TBalancer, etc. |
Snapshots (Data Models)
All data models use C# required init-only properties and provide static Empty singletons for fallback scenarios:
sealed class TemperatureSnapshot
{
public static TemperatureSnapshot Empty { get; } = new()
{
IsAvailable = false,
Readings = [],
MaxTemperatureCelsius = null,
};
public required bool IsAvailable { get; init; }
public required List<TemperatureReading> Readings { get; init; }
public required double? MaxTemperatureCelsius { get; init; }
}
sealed class TemperatureReading
{
public required string Id { get; init; }
public required string Label { get; init; }
public required string Category { get; init; }
public required double TemperatureCelsius { get; init; }
public required string Source { get; init; }
}
sealed class HardwareListSnapshot
{
public static HardwareListSnapshot Empty { get; } = new()
{
IsAvailable = false,
Cpus = [],
Gpus = [],
};
public required bool IsAvailable { get; init; }
public required List<HardwareDevice> Cpus { get; init; }
public required List<HardwareDevice> Gpus { get; init; }
}
sealed class HardwareDevice
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string Category { get; init; }
public required string HardwareType { get; init; }
public required string Source { get; init; }
}Design decisions:
sealedclasses: No inheritance needed; enables JIT optimizations.requiredinit-only properties: Enforced at compile time โ prevents incomplete construction.Emptysingletons: Used as fallback when the helper binary is missing or crashes. Serialized to JSON as{"isAvailable":false,"readings":[],...}.- JSON serialization: Uses
JsonSerializerDefaults.Webfor camelCase property naming (matching the TypeScript interfaces).
JSON Output Format
Temperature mode (eIslandTemperatureReader.exe):
{
"isAvailable": true,
"readings": [
{
"id": "cpu/0/temperature/0",
"label": "CPU Package",
"category": "cpu",
"temperatureCelsius": 52.3,
"source": "libre-hardware-monitor"
},
{
"id": "gpu-nvidia/0/temperature/0",
"label": "NVIDIA GeForce RTX 4070 GPU Core",
"category": "gpu",
"temperatureCelsius": 41.0,
"source": "libre-hardware-monitor"
}
],
"maxTemperatureCelsius": 52.3
}Hardware list mode (eIslandTemperatureReader.exe hardware-list):
{
"isAvailable": true,
"cpus": [
{
"id": "cpu/0",
"name": "Intel Core i7-13700K",
"category": "cpu",
"hardwareType": "Cpu",
"source": "libre-hardware-monitor"
}
],
"gpus": [
{
"id": "gpu-nvidia/0",
"name": "NVIDIA GeForce RTX 4070",
"category": "gpu",
"hardwareType": "GpuNvidia",
"source": "libre-hardware-monitor"
}
]
}Invocation from Node.js
The main plugin's index.js bridges the .NET helper via synchronous child process execution:
function readHelperSnapshot(args, fallback) {
const readerPath = findTemperatureReader();
if (!readerPath) return fallback;
const result = spawnSync(readerPath, args, {
encoding: 'utf8',
windowsHide: true, // Hide console window on Windows
timeout: 5000, // 5-second hard timeout
});
if (result.status !== 0 || result.error || !result.stdout) {
return fallback;
}
return JSON.parse(result.stdout);
}Binary resolution order:
const temperatureReaderCandidates = [
path.join(__dirname, 'temperature-helper', 'bin', 'Release', 'net10.0', 'eIslandTemperatureReader.exe'),
path.join(__dirname, 'temperature-helper', 'bin', 'Debug', 'net10.0', 'eIslandTemperatureReader.exe'),
];Fallback Behavior:
| Condition | Result |
|---|---|
| Binary not found | emptyTemperatureSnapshot / emptyHardwareListSnapshot |
| Process exits with non-zero status | Fallback |
| Process times out (>5s) | Fallback |
| stdout is empty | Fallback |
| JSON parse error | Fallback |
| Success | Parsed snapshot object |
Dependencies
<!-- eIslandTemperatureReader.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.6" />
</ItemGroup>
</Project>| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Runtime (required on client โ this is NOT a NativeAOT build) |
| LibreHardwareMonitorLib | 0.9.6 | Hardware sensor access via WMI/COM |
Build
The .NET helper is built alongside the native addon:
# Build native addon + .NET helper
npm run build
# Equivalent to:
node-gyp rebuild
dotnet build temperature-helper/eIslandTemperatureReader.csproj -c ReleaseOutput: temperature-helper/bin/Release/net10.0/eIslandTemperatureReader.exe
Source Files
| File | Responsibility |
|---|---|
performance_monitor.c | N-API binding layer, exports getCpu and getMemory |
performance_core.c | Core implementation using Win32 APIs |
performance_types.h | Shared type definitions (CpuSnapshot, MemorySnapshot) |
index.js | Entry point, loads native binding, adds getTemperature/getHardwareList |
index.d.ts | TypeScript type declarations |
temperature-helper/Program.cs | .NET CLI entry point, opens Computer, dispatches to collectors |
temperature-helper/TemperatureCollector.cs | Recursive temperature sensor collection |
temperature-helper/HardwareListCollector.cs | CPU/GPU device enumeration |
temperature-helper/HardwareCategoryMapper.cs | HardwareType โ category string mapping |
temperature-helper/Snapshots.cs | Data models with Empty fallback singletons |
Plugin: Windows Bluetooth Helper
Package
@eisland/windows-bluetooth-helperPurpose
Provides Bluetooth device enumeration and real-time connection state monitoring. Used by eIsland to detect paired/connected Bluetooth devices (headphones, speakers, etc.) and respond to connection/disconnection events.
Info
Like the SMTC Helper, this plugin uses the koffi FFI + .NET Native AOT DLL architecture. It wraps the Windows.Devices.Bluetooth and Windows.Devices.Enumeration WinRT APIs.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js + bluetooth-monitor.js + ffi) โ
โ Queries: getPairedDevices / getConnectedDevices / ... โ
โ Monitor: BluetoothMonitor (EventEmitter) via koffi FFI โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ koffi FFI (direct DLL calls)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ eIslandBluetoothCtypes.dll (NativeAOT) โ
โ .NET 10 + Windows.Devices.Bluetooth โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ BtExports.cs โ C-style exported functions โ
โ BluetoothController.cs โ Device enumeration & queries โ
โ BluetoothDeviceMonitor.cs โ DeviceWatcher + ConnectionStatusโ
โ BtJsonContext.cs โ Source-generated JSON serializationโ
โ Snapshots.cs โ Data models โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโTips
The plugin uses two DeviceWatchers (classic Bluetooth + BLE) to cover all device types. Connection state changes are tracked via BluetoothDevice.ConnectionStatusChanged events, not just DeviceWatcher enumeration events.
Exported Functions
Queries
| Function | Signature | Description |
|---|---|---|
getPairedDevices | () โ BluetoothDeviceInfo[] | All paired Bluetooth devices |
getConnectedDevices | () โ BluetoothDeviceInfo[] | Currently connected devices |
getAllDevices | () โ BluetoothDeviceInfo[] | All visible devices (paired + nearby BLE) |
getDevice | (deviceId: string) โ BluetoothDeviceInfo | null | Single device snapshot by ID |
BluetoothMonitor
| Export | Type | Description |
|---|---|---|
BluetoothMonitor | class extends EventEmitter | Real-time Bluetooth device monitor |
Events:
| Event | Callback | Description |
|---|---|---|
device-added | (device: BluetoothDeviceInfo) => void | New device discovered |
device-removed | (deviceId: string) => void | Device no longer visible |
device-connected | (device: BluetoothDeviceInfo) => void | Device connected |
device-disconnected | (deviceId: string) => void | Device disconnected |
device-updated | (device: BluetoothDeviceInfo) => void | Device properties changed |
Result Structure
interface BluetoothDeviceInfo {
deviceId: string; // Windows DeviceInformation ID
name: string | null; // Device friendly name
bluetoothAddress: string | null; // MAC address as hex string
isConnected: boolean; // Currently connected
isPaired: boolean; // Paired with this PC
signalStrength: number | null; // RSSI in dBm
deviceClass: number | null; // Class of Device (CoD)
appearance: number | null; // BLE appearance category
serviceUuids: string[]; // GATT service UUIDs
}Key Implementation Details
Device Discovery:
The plugin uses BluetoothDevice.GetDeviceSelector() and BluetoothLEDevice.GetDeviceSelector() to get the correct AQS filter strings, then creates two DeviceWatcher instances to cover both classic and BLE devices:
// Classic Bluetooth watcher
_classicWatcher = DeviceInformation.CreateWatcher(BluetoothController.GetClassicSelector());
// BLE watcher
_bleWatcher = DeviceInformation.CreateWatcher(BluetoothController.GetBleSelector());Warning
Hardcoded AQS filters like System.Devices.Aep.ProtocolId:="{e0cbf06c-...}" do not work reliably. Always use the WinRT selector methods.
Connection Status Monitoring:
DeviceWatcher only reports enumeration changes (device appeared/disappeared). Connection state changes require subscribing to BluetoothDevice.ConnectionStatusChanged:
var bt = BluetoothDevice.FromIdAsync(deviceId).GetAwaiter().GetResult();
bt.ConnectionStatusChanged += (_, _) =>
{
RefreshDeviceJson(deviceId);
SignalChange();
};NativeAOT Property Limitation:
In NativeAOT, string[] and List<string> cannot be converted to WinRT IIterable<string>. The plugin works around this by not passing additionalProperties to FindAllAsync, and instead enriching device data via BluetoothDevice.FromIdAsync().
Source Files
| File | Responsibility |
|---|---|
src/Snapshots.cs | BluetoothDeviceInfo data model |
src/BluetoothController.cs | Device enumeration, GetDeviceSelector(), BuildDeviceInfo() |
src/BluetoothDeviceMonitor.cs | Dual DeviceWatcher engine + ConnectionStatusChanged subscriptions |
bt-ctypes/BtExports.cs | C-style exported functions for koffi FFI |
bt-ctypes/BtJsonContext.cs | JSON source generator for NativeAOT serialization |
ffi-loader.js | koffi FFI loader โ defines all DLL function signatures |
bluetooth-monitor.js | BluetoothMonitor EventEmitter โ wraps DLL monitoring into Node.js events |
index.js | Public API โ exports queries + BluetoothMonitor |
Build
cd plugins/eisland-windows-bluetooth-helper
npm run build # dotnet build src/eIslandBluetoothHelper.csproj
npm run build:ctypes # dotnet publish bt-ctypes/... (NativeAOT DLL)
npm run build:all # BothDependencies
| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Build-time only (self-contained DLL โ no runtime needed on client) |
| Windows 10 SDK | 10.0.19041.0+ | WinRT API projections for Windows.Devices.Bluetooth |
| koffi | ^2.9.1 | FFI library for calling DLL from Node.js |
Self-Contained Deployment
All .NET Native AOT DLLs (Bluetooth, Power, WiFi, SMTC) are built with <SelfContained>true</SelfContained> and <StaticICU>true</StaticICU>. Client machines do not need the .NET runtime installed.
Performance
| Metric | Value |
|---|---|
| Query latency | ~10โ50ms (WinRT FindAllAsync + FromIdAsync) |
| Monitor startup | ~100ms (two DeviceWatcher.Start() calls) |
| Event detection | Event-driven (zero polling) |
| Memory | Single DLL loaded once; ~5MB resident |
Exported C Functions (ctypes DLL)
| Function | Signature | Description |
|---|---|---|
bt_free_string | (void*) โ void | Free a CoTaskMem-allocated string |
bt_get_last_error | () โ char* | Get last error message |
bt_get_paired_devices | () โ char* | JSON array of paired devices |
bt_get_connected_devices | () โ char* | JSON array of connected devices |
bt_get_all_devices | () โ char* | JSON array of all visible devices |
bt_get_device | (char* id) โ char* | JSON object of single device |
bt_start_monitoring | () โ int | Start device watcher. 0=success |
bt_stop_monitoring | () โ int | Stop watcher. 0=success |
bt_wait_for_changes | (int timeoutMs) โ int | Block until change. 0=changed, 1=timeout |
bt_get_changes_count | () โ int | Read change counter (atomic) |
bt_get_monitored_devices | () โ char* | JSON array from watcher cache |
bt_get_monitored_device | (char* id) โ char* | JSON object from watcher cache |
Caution
You must call bt_free_string() on any pointer returned by bt_get_* functions to avoid memory leaks. The koffi FFI layer in Node.js handles this automatically.
Plugin: Windows Power Helper
Package
@eisland/windows-power-helperPurpose
Provides real-time battery status and power event monitoring. Used by eIsland to display battery level, detect AC power connection/disconnection, and respond to low battery conditions. Works on both laptops (with battery) and desktops (no battery).
Info
This plugin follows the same koffi FFI + .NET Native AOT DLL architecture as the Bluetooth Helper. It wraps the Windows.System.Power.PowerManager WinRT API.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js + power-monitor.js + ffi) โ
โ Query: getPowerInfo() โ
โ Monitor: PowerMonitor (EventEmitter) via koffi FFI โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ koffi FFI (direct DLL calls)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ eIslandPowerCtypes.dll (NativeAOT) โ
โ .NET 10 + Windows.System.Power.PowerManager โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ PwExports.cs โ C-style exported functions โ
โ PowerController.cs โ Power status queries โ
โ PowerMonitor.cs โ PowerManager event subscriptions โ
โ PwJsonContext.cs โ Source-generated JSON serializationโ
โ PowerInfo.cs โ Data model |
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโTips
The PowerManager class is entirely static โ no instance creation needed. All properties and events are accessed directly via PowerManager.RemainingChargePercent, PowerManager.BatteryStatusChanged, etc.
Exported Functions
Queries
| Function | Signature | Description |
|---|---|---|
getPowerInfo | () โ PowerInfo | null | Current power status snapshot |
PowerMonitor
| Export | Type | Description |
|---|---|---|
PowerMonitor | class extends EventEmitter | Real-time power status monitor |
Events:
| Event | Callback | Description |
|---|---|---|
ac-connected | (info: PowerInfo) => void | AC power connected |
ac-disconnected | (info: PowerInfo) => void | AC power disconnected (running on battery) |
battery-low | (info: PowerInfo) => void | Battery level dropped to 15% or below |
charging | (info: PowerInfo) => void | Started charging |
discharging | (info: PowerInfo) => void | Stopped charging (unplugged or fully charged) |
power-changed | (info: PowerInfo) => void | Any power status change (generic event) |
Result Structure
interface PowerInfo {
remainingChargePercent: number; // 0-100, or 100 on desktops with no battery
batteryStatus: number; // 0=NotPresent, 1=Discharging, 2=Idle, 3=Charging
powerSupplyStatus: number; // 0=NotPresent, 1=Adequate, 2=Inadequate, 3=Unknown
energySaverStatus: number; // 0=Disabled, 1=Off, 2=On
hasBattery: boolean; // false on desktops
isCharging: boolean; // true when batteryStatus is Charging
isOnAcPower: boolean; // true when batteryStatus is not Discharging
}Important
On desktops without a battery, hasBattery is false, batteryStatus is 0 (NotPresent), and remainingChargePercent is 100. The isOnAcPower field uses batteryStatus != Discharging rather than powerSupplyStatus to correctly handle cases where the power supply is "Inadequate" (e.g., a low-wattage charger still provides AC power).
Key Implementation Details
PowerManager API:
The plugin uses the WinRT Windows.System.Power.PowerManager static class. All properties are read-only and event-driven:
var batteryStatus = (int)PowerManager.BatteryStatus;
var remainingChargePercent = PowerManager.RemainingChargePercent;
var energySaverStatus = (int)PowerManager.EnergySaverStatus;Event Subscriptions:
The monitor subscribes to four PowerManager events. Event handler references are stored to prevent GC collection and enable clean unsubscription:
PowerManager.RemainingChargePercentChanged += _chargePercentHandler;
PowerManager.BatteryStatusChanged += _batteryStatusHandler;
PowerManager.PowerSupplyStatusChanged += _powerSupplyHandler;
PowerManager.EnergySaverStatusChanged += _energySaverHandler;Note
Unlike the Bluetooth Helper which requires STA threads for WinRT COM, the PowerManager API works directly on any thread. No RunOnSTAThread wrapper is needed.
Low Battery Detection:
The JavaScript layer implements low battery detection by comparing current and previous states. The threshold is 15% โ the battery-low event fires only when the percentage crosses below this threshold while on battery power (not charging).
Source Files
| File | Responsibility |
|---|---|
src/PowerInfo.cs | PowerInfo data model |
src/PowerController.cs | Power status queries via PowerManager |
src/PowerMonitor.cs | PowerManager event subscriptions + Win32 event signaling |
pw-ctypes/PwExports.cs | C-style exported functions for koffi FFI |
pw-ctypes/PwJsonContext.cs | JSON source generator for NativeAOT serialization |
ffi-loader.js | koffi FFI loader โ defines all DLL function signatures |
power-monitor.js | PowerMonitor EventEmitter โ wraps DLL monitoring into Node.js events |
index.js | Public API โ exports getPowerInfo() + PowerMonitor |
Build
cd plugins/eisland-windows-power-helper
npm run build # dotnet build src/eIslandPowerHelper.csproj
npm run build:ctypes # dotnet publish pw-ctypes/... (NativeAOT DLL)
npm run build:all # BothDependencies
| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Build-time only (self-contained DLL โ no runtime needed on client) |
| Windows 10 SDK | 10.0.19041.0+ | WinRT API projections for Windows.System.Power |
| koffi | ^2.9.1 | FFI library for calling DLL from Node.js |
Performance
| Metric | Value |
|---|---|
| Query latency | ~1ms (in-memory PowerManager property reads) |
| Monitor startup | ~5ms (event subscription only, no watcher) |
| Event detection | Event-driven (zero polling) |
| Memory | Single DLL loaded once; ~4MB resident |
Exported C Functions (ctypes DLL)
| Function | Signature | Description |
|---|---|---|
pw_free_string | (void*) โ void | Free a CoTaskMem-allocated string |
pw_get_last_error | () โ char* | Get last error message |
pw_get_power_info | () โ char* | JSON object of current power status |
pw_start_monitoring | () โ int | Start power monitoring. 0=success |
pw_stop_monitoring | () โ int | Stop monitoring. 0=success |
pw_wait_for_changes | (int timeoutMs) โ int | Block until change. 0=changed, 1=timeout |
pw_get_changes_count | () โ int | Read change counter (atomic) |
pw_get_monitored_power_info | () โ char* | JSON object of latest monitored power status |
Caution
You must call pw_free_string() on any pointer returned by pw_get_* functions to avoid memory leaks. The koffi FFI layer in Node.js handles this automatically.
Plugin: Windows WiFi Helper
Package
@eisland/windows-wifi-helperPurpose
Provides real-time WiFi connection status monitoring. Used by eIsland to detect WiFi connect/disconnect events, track SSID changes, and monitor signal strength. Works on both WiFi-connected laptops and desktops using Ethernet (reports non-WiFi connections with isWifiAdapter: false).
Info
This plugin follows the same koffi FFI + .NET Native AOT DLL architecture as the Bluetooth and Power Helpers. It wraps the Windows.Networking.Connectivity.NetworkInformation WinRT API.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js + wifi-monitor.js + ffi) โ
โ Query: getWifiInfo() โ
โ Monitor: WifiMonitor (EventEmitter) via koffi FFI โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ koffi FFI (direct DLL calls)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ eIslandWifiCtypes.dll (NativeAOT) โ
โ .NET 10 + Windows.Networking.Connectivity โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ WfExports.cs โ C-style exported functions โ
โ WifiController.cs โ Connection profile queries โ
โ WifiMonitor.cs โ NetworkStatusChanged subscriptions โ
โ WfJsonContext.cs โ Source-generated JSON serializationโ
โ WifiInfo.cs โ Data model โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโTips
The plugin uses NetworkInformation.NetworkStatusChanged โ a single event subscription that covers all network changes. WiFi-specific filtering is done by checking NetworkAdapter.IanaInterfaceType == 71 (WiFi).
Exported Functions
Queries
| Function | Signature | Description |
|---|---|---|
getWifiInfo | () โ WifiInfo | null | Current WiFi connection status snapshot |
WifiMonitor
| Export | Type | Description |
|---|---|---|
WifiMonitor | class extends EventEmitter | Real-time WiFi status monitor |
Events:
| Event | Callback | Description |
|---|---|---|
wifi-connected | (info: WifiInfo) => void | WiFi connected |
wifi-disconnected | (info: WifiInfo) => void | WiFi disconnected |
ssid-changed | (info: WifiInfo) => void | SSID changed (network switch) |
signal-changed | (info: WifiInfo) => void | Signal strength changed |
wifi-changed | (info: WifiInfo) => void | Any WiFi status change (generic event) |
Result Structure
interface WifiInfo {
isConnected: boolean; // Whether connected to a network
ssid: string | null; // WiFi network name, null if not connected or not WiFi
signalBars: number; // 0-5 bars, -1 if unavailable
connectivityLevel: number; // 0=None, 1=LocalAccess, 2=ConstrainedInternet, 3=InternetAccess
adapterName: string | null; // Network adapter GUID, null if no adapter
isWifiAdapter: boolean; // true if adapter is WiFi (IANA type 71)
}Important
When connected via Ethernet (not WiFi), isWifiAdapter is false and ssid is null. The isConnected and connectivityLevel fields still reflect the Ethernet connection state. This allows the plugin to be useful on desktop machines without WiFi hardware.
Key Implementation Details
NetworkInformation API:
The plugin uses NetworkInformation.GetInternetConnectionProfile() to get the active connection profile, then inspects the NetworkAdapter to determine if it is WiFi:
var profile = NetworkInformation.GetInternetConnectionProfile();
var adapter = profile.NetworkAdapter;
var isWifi = adapter?.IanaInterfaceType == 71;
var connectivityLevel = (int)profile.GetNetworkConnectivityLevel();Event Subscription:
A single NetworkStatusChanged event covers all network state changes. The JavaScript layer diffs previous and current state to emit specific events:
NetworkInformation.NetworkStatusChanged += _statusChangedHandler;Note
Like the Power Helper, the NetworkInformation API works directly on any thread โ no STA thread wrapper is needed. This simplifies the monitor implementation compared to the Bluetooth Helper.
SSID Extraction:
The SSID is extracted via ConnectionProfile.WlanConnectionProfileDetails.GetConnectedSsid(). This method is only available for WiFi connections; for Ethernet or other adapters, it returns null.
Source Files
| File | Responsibility |
|---|---|
src/WifiInfo.cs | WifiInfo data model |
src/WifiController.cs | Connection profile queries via NetworkInformation |
src/WifiMonitor.cs | NetworkStatusChanged event subscription + Win32 event signaling |
wf-ctypes/WfExports.cs | C-style exported functions for koffi FFI |
wf-ctypes/WfJsonContext.cs | JSON source generator for NativeAOT serialization |
ffi-loader.js | koffi FFI loader โ defines all DLL function signatures |
wifi-monitor.js | WifiMonitor EventEmitter โ wraps DLL monitoring into Node.js events |
index.js | Public API โ exports getWifiInfo() + WifiMonitor |
Build
cd plugins/eisland-windows-wifi-helper
npm run build # dotnet build src/eIslandWifiHelper.csproj
npm run build:ctypes # dotnet publish wf-ctypes/... (NativeAOT DLL)
npm run build:all # BothDependencies
| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Build-time only (self-contained DLL โ no runtime needed on client) |
| Windows 10 SDK | 10.0.19041.0+ | WinRT API projections for Windows.Networking.Connectivity |
| koffi | ^2.9.1 | FFI library for calling DLL from Node.js |
Performance
| Metric | Value |
|---|---|
| Query latency | ~1ms (in-memory NetworkInformation profile reads) |
| Monitor startup | ~5ms (single event subscription) |
| Event detection | Event-driven (zero polling) |
| Memory | Single DLL loaded once; ~4MB resident |
Exported C Functions (ctypes DLL)
| Function | Signature | Description |
|---|---|---|
wf_free_string | (void*) โ void | Free a CoTaskMem-allocated string |
wf_get_last_error | () โ char* | Get last error message |
wf_get_wifi_info | () โ char* | JSON object of current WiFi status |
wf_start_monitoring | () โ int | Start WiFi monitoring. 0=success |
wf_stop_monitoring | () โ int | Stop monitoring. 0=success |
wf_wait_for_changes | (int timeoutMs) โ int | Block until change. 0=changed, 1=timeout |
wf_get_changes_count | () โ int | Read change counter (atomic) |
wf_get_monitored_wifi_info | () โ char* | JSON object of latest monitored WiFi status |
Caution
You must call wf_free_string() on any pointer returned by wf_get_* functions to avoid memory leaks. The koffi FFI layer in Node.js handles this automatically.
Plugin: Windows Brightness Helper
Package
@eisland/windows-brightness-helperPurpose
Provides screen brightness query, control, and real-time monitoring via WMI. Used by eIsland to display and adjust the user's screen brightness, and to react to brightness changes made through hardware keys or system settings.
Info
Unlike the Bluetooth, Power, and WiFi Helpers which use koffi FFI + NativeAOT DLL, the Brightness Helper uses a .NET console EXE spawned via spawnSync (queries) and spawn (monitoring). This is because System.Management (WMI) is incompatible with NativeAOT โ the COM interop layer breaks during AOT compilation.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js) โ
โ Queries: getBrightness() / setBrightness() via spawnSync โ
โ Monitor: BrightnessMonitor (EventEmitter) via spawn โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ spawnSync / spawn (child process)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ eIslandBrightnessReader.exe (.NET 10 Console App) โ
โ System.Management + WMI (root\wmi) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Program.cs โ
โ CLI entry point: get / set <n> / monitor โ
โ BrightnessHelper (static class) โ
โ โโ GetBrightness() โ WmiMonitorBrightness โ
โ โโ SetBrightness(n) โ WmiMonitorBrightnessMethods โ
โ โโ Monitor() โ WmiMonitorBrightnessEvent โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโTips
The monitor command uses ManagementEventWatcher to subscribe to WmiMonitorBrightnessEvent โ a true WMI event subscription. The .NET process blocks until an event fires, then writes a JSON line to stdout. The Node.js BrightnessMonitor reads these lines via spawn stdout data events. This is event-driven, not polling.
Exported Functions
Queries
| Function | Signature | Description |
|---|---|---|
getBrightness | () โ BrightnessInfo | null | Current screen brightness snapshot |
setBrightness | (brightness: number) โ boolean | Set brightness (0โ100), clamped. Returns true on success |
BrightnessMonitor
| Export | Type | Description |
|---|---|---|
BrightnessMonitor | class extends EventEmitter | Real-time brightness monitor via WMI events |
Events:
| Event | Callback | Description |
|---|---|---|
brightness-changed | (brightness: number, timestamp: number) => void | Brightness changed (hardware key, system setting, or API) |
error | (err: Error) => void | Monitor process error |
Result Structure
interface BrightnessInfo {
currentBrightness: number; // 0โ100, current brightness percentage
levels: number[] | null; // Supported brightness levels (0โ100), typically 0โ100 in steps of 1
instanceName: string | null; // WMI monitor instance name (e.g., "DISPLAY\BOE381E\5&...")
}Important
The levels array contains all supported brightness values. Most laptop displays support 101 levels (0โ100). External monitors connected via DDC/CI may support fewer levels or none at all. If WMI brightness data is unavailable (e.g., desktop with no backlight), getBrightness() returns null.
CLI Modes
The .NET helper is a CLI tool invoked with a single argument:
| Invocation | Mode | Output |
|---|---|---|
eIslandBrightnessReader.exe get | Query (default) | BrightnessInfo JSON |
eIslandBrightnessReader.exe set <0-100> | Set brightness | { success: true, brightness: N } JSON |
eIslandBrightnessReader.exe monitor | Event monitor | Stream of { brightness, timestamp } JSON lines |
Monitor output format (one line per brightness change):
{"brightness":75,"timestamp":1719700000000}The monitor process stays alive until stdin is closed or a termination signal is received. The Node.js layer kills the process on monitor.stop().
Key Implementation Details
WMI Queries:
// Get current brightness
using var searcher = new ManagementObjectSearcher(@"root\wmi", "SELECT * FROM WmiMonitorBrightness");
foreach (ManagementObject obj in searcher.Get())
{
var currentBrightness = (byte)obj["CurrentBrightness"];
var levels = obj["Level"] as byte[];
var instanceName = obj["InstanceName"] as string;
}
// Set brightness
using var searcher = new ManagementObjectSearcher(@"root\wmi", "SELECT * FROM WmiMonitorBrightnessMethods");
foreach (ManagementObject obj in searcher.Get())
{
var inParams = obj.GetMethodParameters("WmiSetBrightness");
inParams["Brightness"] = brightness;
inParams["Timeout"] = (uint)0;
obj.InvokeMethod("WmiSetBrightness", inParams, null);
}WMI Event Subscription:
using var watcher = new ManagementEventWatcher(@"root\wmi", "SELECT * FROM WmiMonitorBrightnessEvent");
watcher.EventArrived += (sender, e) =>
{
var brightness = (byte)e.NewEvent.Properties["Brightness"].Value;
Console.WriteLine(JsonSerializer.Serialize(new { brightness, timestamp }));
};
watcher.Start();Warning
System.Management uses COM interop internally. The Timeout parameter in WmiSetBrightness must be cast to uint, not int, or the WMI call fails with a type mismatch error.
Why Not NativeAOT?
Both System.Management and Microsoft.Management.Infrastructure produce fatal COM interop errors under NativeAOT:
System.Management:WbemContext..ctor()always throws โ the Wbem COM classes cannot be instantiatedMicrosoft.Management.Infrastructure:DeserializerCallbacksdelegate marshalling data is missing
The .NET console EXE approach avoids this entirely โ System.Management works perfectly under the standard .NET runtime.
Source Files
| File | Responsibility |
|---|---|
src/eIslandBrightnessReader.csproj | .NET 10 console app project file |
src/Program.cs | CLI entry point (get/set/monitor) + BrightnessHelper static class |
index.js | Public API โ getBrightness(), setBrightness(), BrightnessMonitor class |
index.d.ts | TypeScript type declarations |
Build
cd plugins/eisland-windows-brightness-helper
npm run build # dotnet build src/eIslandBrightnessReader.csproj -c ReleaseOutput: src/bin/Release/net10.0/eIslandBrightnessReader.exe
Dependencies
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Management" Version="10.0.0-preview.3.25171.5" />
</ItemGroup>
</Project>| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Runtime (required on client โ this is NOT a self-contained build; WMI is incompatible with NativeAOT) |
| System.Management | 10.0.0-preview.3 | WMI access (ManagementObjectSearcher, ManagementEventWatcher) |
Performance
| Metric | Value |
|---|---|
| Query latency | ~100โ300ms (process spawn + WMI query) |
| Set latency | ~100โ300ms (process spawn + WMI method invocation) |
| Monitor startup | ~100ms (process spawn + WMI event subscription) |
| Event detection | Event-driven (WMI WmiMonitorBrightnessEvent, zero polling) |
| Memory | Process exits after each query; monitor process ~15MB resident |
Note
Query and set operations spawn a new .NET process each time, incurring ~100ms startup overhead. For frequent polling, consider using the BrightnessMonitor event-driven approach instead of repeated getBrightness() calls.
Testing
Test Framework
Plugins with tests use Vitest with the following configuration:
// vitest.config.ts
export default defineConfig({
test: {
environment: 'node',
include: ['test/**/*.test.ts'],
clearMocks: true,
restoreMocks: true,
},
});Test Commands
# Run unit tests
npm test
# Run smoke tests (manual verification)
npm run smoke
npm run smoke:pollingTesting Patterns
Shape Validation Tests:
Tests verify that exported functions return objects with the correct structure and types:
it('returns CPU snapshot shape after baseline warmup', () => {
monitor.getCpu(); // First call: baseline
const cpu = monitor.getCpu(); // Second call: has delta
expect(cpu.usagePercent).toBeTypeOf('number');
expect(cpu.hasBaseline).toBe(true);
expect(cpu.usagePercent).toBeGreaterThanOrEqual(0);
expect(cpu.usagePercent).toBeLessThanOrEqual(100);
});Export Verification:
Tests confirm all expected functions are exported:
it('exports detector methods', () => {
expect(typeof detector.getForegroundFullscreenWindow).toBe('function');
expect(typeof detector.getFullscreenWindows).toBe('function');
expect(typeof detector.isAnyFullscreenWindow).toBe('function');
});Nullable Result Handling:
Tests handle functions that may return null depending on system state:
const foreground = detector.getForegroundFullscreenWindow();
expect(foreground === null || typeof foreground === 'object').toBe(true);Test Coverage by Plugin
| Plugin | Test Files | Test Types |
|---|---|---|
| Processes Attacker | (manual testing) | โ |
| Fullscreen Detector | fullscreen-detector.test.ts, fullscreen-detector.polling.test.ts | Shape validation, export verification |
| Performance Monitor | performance-monitor.test.ts | Shape validation, range checks |
| SMTC Helper | smtc-helper.test.ts, smtc-helper.play.test.ts, smtc-helper.pause.test.ts, smtc-helper.next.test.ts, smtc-helper.previous.test.ts | Shape validation, export verification, command tests |
| Bluetooth Helper | bluetooth.test.ts, bluetooth.monitor.test.ts | Shape validation, export verification, monitor state management |
| Brightness Helper | brightness.test.ts, brightness.monitor.test.ts | Shape validation, export verification, boundary value tests, monitor state management |
Integration with Electron
Loading Native Plugins
Plugins are loaded in the Electron main process via require():
// Example: Loading the fullscreen detector
const detector = require('@eisland/windows-fullscreen-detector');
// Check if any window is fullscreen
if (detector.isAnyFullscreenWindow()) {
// Auto-hide the island
mainWindow.hide();
}IPC Domain Integration
Each plugin's capabilities are exposed to the renderer via IPC handlers:
// Example: Process close via IPC
registerProcessHandlers({ getMainWindow });
// In the handler:
ipcMain.handle('win.close', async (_event, target) => {
const result = closeProcess(target);
return result;
});Usage in AI Agent Tools
Plugins power several AI agent tools:
| Tool | Plugin | Function Used |
|---|---|---|
win.close | Processes Attacker | closeProcess(target) |
win.list | Fullscreen Detector | getFullscreenWindows() |
monitor.cpu | Performance Monitor | getCpu() |
monitor.memory | Performance Monitor | getMemory() |
Performance Characteristics
Tips
Performance characteristics and optimization recommendations for each plugin:
| Plugin | Call Latency | Notes |
|---|---|---|
| Processes Attacker | ~1โ5ms | Snapshot-based enumeration, single TerminateProcess call per match |
| Fullscreen Detector | ~1โ3ms | EnumWindows iterates all top-level windows; early-exit for isAnyFullscreenWindow |
| Performance Monitor (CPU) | <0.1ms | GetSystemTimes is a single kernel call |
| Performance Monitor (Memory) | <0.1ms | GlobalMemoryStatusEx is a single kernel call |
| Performance Monitor (Temperature) | ~500โ2000ms | Spawns a .NET process; includes process startup + WMI queries |
| SMTC Helper | ~50โ200ms | Spawns a .NET process; WinRT session manager + media property reads |
| Bluetooth Helper | ~10โ50ms | WinRT FindAllAsync + FromIdAsync; event-driven monitoring |
| Brightness Helper | ~100โ300ms | Process spawn + WMI query; event-driven monitoring |
Optimization Notes:
- CPU and memory snapshots are nearly zero-cost โ suitable for polling at 1-second intervals
- Temperature queries are expensive due to the .NET helper process โ recommended at 5โ10 second intervals
- The fullscreen detector avoids COM/WMI โ pure Win32 window API calls
- SMTC commands are direct DLL calls (~1-5ms via koffi FFI); monitoring is event-driven (zero polling)
Warning
Temperature queries involve .NET process startup and WMI queries, resulting in high latency (500โ2000ms). Recommended polling interval is 5โ10 seconds to avoid frequent calls impacting performance.
Plugin: Windows SMTC Helper
Package
@eisland/windows-smtc-helperPurpose
Provides control over Windows System Media Transport Controls (SMTC) โ the standard media playback interface that appears in the Windows notification center and on the lock screen. Used by eIsland to display and control currently playing media from any source (Spotify, QQ Music, Chrome, etc.).
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript Layer (index.js + smtc-monitor.js + ffi-loader) โ
โ Commands: play / pause / next / previous / seek / stop ... โ
โ Monitor: SmtcMonitor (EventEmitter) via koffi FFI โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ koffi FFI (direct DLL calls)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ eIslandSmtcCtypes.dll (NativeAOT) โ
โ .NET 10 + Windows.Media.Control โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ SmtcExports.cs โ C-style exported functions โ
โ SmtcController.cs โ Command execution (play/pause/โฆ) โ
โ SmtcSessionMonitor.cs โ Event-driven session monitoring โ
โ SmtcJsonContext.cs โ Source-generated JSON serializationโ
โ Snapshots.cs โ Data models โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ โฒ
โ โ
โโโโโโโดโโโโโโโ โโโโโโโโโดโโโโโโโโโ
โ Node.js โ โ Python / C / โ
โ (koffi) โ โ any FFI lang โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโInfo
Unlike other plugins that use C + N-API, the SMTC Helper is a pure C# .NET application compiled as a NativeAOT DLL. Node.js accesses it via koffi (a zero-compile FFI library), eliminating native addon build issues. The DLL also works from Python ctypes or any language with C FFI support.
Tips
The plugin provides two integration modes:
- Commands โ synchronous function calls (
play,pause,seek, etc.) for media control - Monitor โ event-driven session tracking (
SmtcMonitorclass) for real-time state updates, replacing the need for a separate monitoring package
Exported Functions
Commands
| Function | Signature | Description |
|---|---|---|
play | () โ CommandResult | Resume playback of the current media session |
pause | () โ CommandResult | Pause the current media session |
next | () โ CommandResult | Skip to the next track |
previous | () โ CommandResult | Skip to the previous track |
getStatus | () โ MediaStatus | Get the full snapshot of the current media session |
getTimestamp | () โ TimestampInfo | Lightweight playback timestamp (no media metadata), optimized for lyrics calibration |
seek | (seconds: number) โ CommandResult | Seek to a position in seconds |
stop | () โ CommandResult | Stop playback |
setShuffle | (active: boolean) โ CommandResult | Toggle shuffle mode |
setRepeatMode | (mode: number) โ CommandResult | Set repeat mode (0=None, 1=Track, 2=List) |
setPlaybackRate | (rate: number) โ CommandResult | Set playback rate (1.0 = normal) |
SmtcMonitor
| Export | Type | Description |
|---|---|---|
SmtcMonitor | class extends EventEmitter | Real-time SMTC session monitor |
Events (compatible with @coooookies/windows-smtc-monitor):
| Event | Callback | Description |
|---|---|---|
session-added | (sourceAppId: string, media: MediaProps) => void | New media session detected |
session-removed | (sourceAppId: string) => void | Media session closed |
session-media-changed | (sourceAppId: string, media: MediaProps) => void | Track metadata changed |
session-playback-changed | (sourceAppId: string, playback: PlaybackInfo) => void | Play/pause/stop state changed |
session-timeline-changed | (sourceAppId: string, timeline: TimelineProps) => void | Playback position updated |
MediaStatus
interface MediaStatus {
isAvailable: boolean;
title: string | null;
artist: string | null;
albumTitle: string | null;
albumArtist: string | null;
trackNumber: number | null;
genres: string[] | null;
playbackStatus: 'playing' | 'paused' | 'stopped' | 'closed' | 'opened' | 'changing' | 'unknown';
isShuffleActive: boolean | null;
repeatMode: number | null;
playbackRate: number | null;
sourceAppUserModelId: string | null;
thumbnail: string | null; // data:image/jpeg;base64,...
timeline: TimelineProperties | null;
controls: PlaybackControls | null;
}
interface TimelineProperties {
startTime: number; // seconds
endTime: number; // seconds
position: number; // seconds
minSeekTime: number; // seconds
maxSeekTime: number; // seconds
}
interface PlaybackControls {
isPlayEnabled: boolean;
isPauseEnabled: boolean;
isNextEnabled: boolean;
isPreviousEnabled: boolean;
isStopEnabled: boolean;
isRecordEnabled: boolean;
isFastForwardEnabled: boolean;
isRewindEnabled: boolean;
isChannelUpEnabled: boolean;
isChannelDownEnabled: boolean;
}
interface CommandResult {
success: boolean;
error: string | null;
}
interface TimestampInfo {
isAvailable: boolean;
playbackStatus: 'playing' | 'paused' | 'stopped' | 'closed' | 'opened' | 'changing' | 'unknown';
timeline: TimelineProperties | null;
}Tips
getTimestamp() is a lightweight alternative to getStatus() โ it returns only playback status and timeline position without fetching media metadata (title, artist, album art, etc.). This makes it ideal for lyrics calibration where only the playback position is needed.
C# Implementation
SmtcController
The core controller uses GlobalSystemMediaTransportControlsSessionManager to access the active media session:
public static class SmtcController
{
private static async Task<GlobalSystemMediaTransportControlsSession?> GetCurrentSessionAsync()
{
var manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
return manager.GetCurrentSession();
}
public static async Task<CommandResult> PlayAsync()
{
var session = await GetCurrentSessionAsync();
if (session == null)
return CommandResult.Fail("No active media session.");
var success = await session.TryPlayAsync();
return success ? CommandResult.Ok : CommandResult.Fail("Play command was rejected.");
}
// Pause, Next, Previous follow the same pattern...
}Key behaviors:
RequestAsync()acquires the session manager โ requires a UI thread orCoreApplicationcontextGetCurrentSession()returnsnullif no app is currently playing mediaTryPlayAsync()/TryPauseAsync()returnfalseif the media source rejects the commandSourceAppUserModelIdidentifies the media source (e.g.,"QQMusic.exe","Spotify.exe")- Thumbnail is read as a stream, converted to base64, and returned as a
data:image/jpeg;base64,...data URI
Timeline Properties
Timeline data is read from session.GetTimelineProperties() and converted to seconds:
var timeline = session.GetTimelineProperties();
var timelineProperties = new TimelineProperties
{
StartTime = timeline.StartTime.TotalSeconds,
EndTime = timeline.EndTime.TotalSeconds,
Position = timeline.Position.TotalSeconds,
MinSeekTime = timeline.MinSeekTime.TotalSeconds,
MaxSeekTime = timeline.MaxSeekTime.TotalSeconds,
};Note
Some media sources (like QQ Music) report minSeekTime and maxSeekTime as 0, even though seeking works. This is a limitation of the media source's SMTC implementation, not the plugin.
Playback Controls
The PlaybackControls object reports which transport buttons the media source has enabled:
var controls = new PlaybackControls
{
IsPlayEnabled = playbackInfo.Controls.IsPlayEnabled,
IsPauseEnabled = playbackInfo.Controls.IsPauseEnabled,
IsNextEnabled = playbackInfo.Controls.IsNextEnabled,
IsPreviousEnabled = playbackInfo.Controls.IsPreviousEnabled,
// ... and so on
};SmtcMonitor Usage
import { SmtcMonitor } from '@eisland/windows-smtc-helper';
const monitor = new SmtcMonitor();
monitor.on('session-added', (sourceAppId, media) => {
console.log(`Now playing: ${media.title} โ ${media.artist}`);
});
monitor.on('session-playback-changed', (sourceAppId, playback) => {
console.log(`Status: ${playback.playbackStatus}`); // 4=playing, 5=paused
});
monitor.on('session-timeline-changed', (sourceAppId, timeline) => {
console.log(`Position: ${timeline.position.toFixed(1)}s`);
});
monitor.start();
// ... later
monitor.stop();Note
The SmtcMonitor uses WinRT event callbacks (MediaPropertiesChanged, PlaybackInfoChanged, TimelinePropertiesChanged) for zero-polling real-time updates. Timeline events are throttled to 200ms intervals. The getMediaSessions() method returns a synchronous snapshot of all active sessions.
CLI Modes
| Invocation | Mode | Output |
|---|---|---|
eIslandSmtcHelper.exe status | Status (default) | MediaStatus JSON |
eIslandSmtcHelper.exe play | Play | CommandResult JSON |
eIslandSmtcHelper.exe pause | Pause | CommandResult JSON |
eIslandSmtcHelper.exe next | Next | CommandResult JSON |
eIslandSmtcHelper.exe previous | Previous | CommandResult JSON |
eIslandSmtcHelper.exe seek <seconds> | Seek | CommandResult JSON |
eIslandSmtcHelper.exe stop | Stop | CommandResult JSON |
| `eIslandSmtcHelper.exe set-shuffle <0 | 1>` | Shuffle |
| `eIslandSmtcHelper.exe set-repeat-mode <0 | 1 | 2>` |
eIslandSmtcHelper.exe set-playback-rate <rate> | Rate | CommandResult JSON |
Source Files
| File | Responsibility |
|---|---|
src/Program.cs | CLI entry point, argument parsing, JSON output |
src/SmtcController.cs | SMTC command execution (play, pause, seek, stop, shuffle, repeat, rate) |
src/SmtcSessionMonitor.cs | Event-driven session monitoring engine (WinRT callbacks + Win32 events) |
src/Snapshots.cs | Data models (MediaStatus, CommandResult, SessionInfo, MediaMetadata, etc.) |
ffi-loader.js | koffi FFI loader โ defines all DLL function signatures |
smtc-monitor.js | SmtcMonitor EventEmitter โ wraps DLL monitoring into Node.js events |
index.js | Public API โ exports commands + SmtcMonitor |
Build
cd plugins/eisland-windows-smtc-helper
npm run build # Runs: dotnet build src/eIslandSmtcHelper.csproj -c ReleaseOutput: src/bin/Release/net10.0-windows10.0.19041.0/eIslandSmtcHelper.exe
Warning
The SMTC helper targets net10.0-windows10.0.19041.0 to access WinRT APIs. This requires the Windows 10 SDK (10.0.19041.0) or later installed alongside the .NET 10 SDK.
Dependencies
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
</PropertyGroup>
</Project>| Dependency | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Build-time only (self-contained DLL โ no runtime needed on client) |
| Windows 10 SDK | 10.0.19041.0+ | WinRT API projections for Windows.Media.Control |
Self-Contained Deployment
The SMTC Helper DLL is built with <SelfContained>true</SelfContained> and <StaticICU>true</StaticICU>, so all .NET and ICU dependencies are statically linked. Client machines do not need the .NET runtime installed.
Example Output
getStatus() with QQ Music playing:
{
"isAvailable": true,
"title": "Chasing Gold and Gloory",
"artist": "Gruup24",
"albumTitle": "Chasing Gold and Gloory",
"albumArtist": "",
"trackNumber": 0,
"genres": null,
"playbackStatus": "playing",
"isShuffleActive": null,
"repeatMode": null,
"playbackRate": null,
"sourceAppUserModelId": "QQMusic.exe",
"thumbnail": "data:image/jpeg;base64,/9j/4AAQ...",
"timeline": {
"startTime": 0,
"endTime": 229.277,
"position": 134.091,
"minSeekTime": 0,
"maxSeekTime": 0
},
"controls": {
"isPlayEnabled": false,
"isPauseEnabled": true,
"isNextEnabled": true,
"isPreviousEnabled": true,
"isStopEnabled": false,
"isRecordEnabled": false,
"isFastForwardEnabled": false,
"isRewindEnabled": false,
"isChannelUpEnabled": false,
"isChannelDownEnabled": false
}
}ctypes DLL (NativeAOT)
The plugin also ships a NativeAOT-compiled DLL (eIslandSmtcCtypes.dll) that exports C-style functions for use from Python, C, or any language with FFI support.
Build
cd plugins/eisland-windows-smtc-helper
npm run build:ctypes # dotnet publish smtc-ctypes/eIslandSmtcCtypes.csproj -c Release -r win-x64Output: smtc-ctypes/bin/Release/net10.0-windows10.0.19041.0/win-x64/publish/eIslandSmtcCtypes.dll
Warning
Building the NativeAOT DLL requires vswhere.exe in PATH. If the build fails with 'vswhere.exe' is not recognized, add it:
export PATH="/c/Program Files (x86)/Microsoft Visual Studio/Installer:$PATH"Exported C Functions
Commands:
| Function | Signature | Description |
|---|---|---|
smtc_play | () โ int | Play. Returns 0=success, 1=failure |
smtc_pause | () โ int | Pause. Returns 0=success, 1=failure |
smtc_next | () โ int | Next track. Returns 0=success, 1=failure |
smtc_previous | () โ int | Previous track. Returns 0=success, 1=failure |
smtc_seek | (double seconds) โ int | Seek to position. Returns 0=success |
smtc_stop | () โ int | Stop playback. Returns 0=success |
smtc_set_shuffle | (int active) โ int | Set shuffle (0=off, 1=on). Returns 0=success |
smtc_set_repeat_mode | (int mode) โ int | Set repeat (0=None, 1=Track, 2=List). Returns 0=success |
smtc_set_playback_rate | (double rate) โ int | Set playback rate. Returns 0=success |
smtc_get_status | () โ char* | Get status JSON. Returns NULL on failure |
smtc_get_timestamp | () โ char* | Get lightweight timestamp JSON (no media metadata). Returns NULL on failure |
smtc_free_string | (char*) โ void | Free a string returned by status/error functions |
smtc_get_last_error | () โ char* | Get last error message |
Monitoring:
| Function | Signature | Description |
|---|---|---|
smtc_start_monitoring | () โ int | Start session monitoring. Returns 0=success |
smtc_stop_monitoring | () โ int | Stop session monitoring. Returns 0=success |
smtc_wait_for_changes | (int timeoutMs) โ int | Block until change or timeout. 0=changed, 1=timeout |
smtc_get_sessions_changed | () โ int | Read change counter (atomic) |
smtc_get_all_sessions | () โ char* | Get all sessions as JSON array |
smtc_get_session | (char* appId) โ char* | Get specific session by source app ID |
Caution
You must call smtc_free_string() on any pointer returned by smtc_get_status(), smtc_get_timestamp(), smtc_get_last_error(), smtc_get_all_sessions(), or smtc_get_session() to avoid memory leaks. The koffi FFI layer in Node.js handles this automatically.
Python Usage
import ctypes, json
dll = ctypes.CDLL("./eIslandSmtcCtypes.dll")
dll.smtc_get_status.restype = ctypes.c_void_p
dll.smtc_get_timestamp.restype = ctypes.c_void_p
dll.smtc_free_string.argtypes = [ctypes.c_void_p]
# Full status (includes media metadata)
ptr = dll.smtc_get_status()
if ptr:
status = json.loads(ctypes.string_at(ptr).decode("utf-8"))
dll.smtc_free_string(ptr)
print(status["title"], status["artist"])
# Lightweight timestamp (playback position only, no metadata)
ptr = dll.smtc_get_timestamp()
if ptr:
ts = json.loads(ctypes.string_at(ptr).decode("utf-8"))
dll.smtc_free_string(ptr)
print(ts["playbackStatus"], ts["timeline"]["position"])
dll.smtc_play() # Returns 0 on success
dll.smtc_pause() # Returns 0 on successNativeAOT Implementation Notes
The NativeAOT DLL has two key implementation details:
1. STA Thread for WinRT
WinRT async APIs require COM STA initialization. Each exported function spawns a dedicated STA thread:
private static T RunOnSTAThread<T>(Func<Task<T>> asyncFunc)
{
T result = default!;
Exception? ex = null;
var thread = new Thread(() =>
{
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
result = asyncFunc().GetAwaiter().GetResult();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (ex != null) throw ex;
return result;
}2. JSON Source Generation
NativeAOT disables reflection-based serialization. A source generator context provides compile-time JSON serialization:
[JsonSerializable(typeof(MediaStatus))]
[JsonSerializable(typeof(CommandResult))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class SmtcJsonContext : JsonSerializerContext { }Source Files (ctypes)
| File | Responsibility |
|---|---|
smtc-ctypes/eIslandSmtcCtypes.csproj | NativeAOT class library project |
smtc-ctypes/SmtcExports.cs | C-style exported functions (commands + monitoring) |
smtc-ctypes/SmtcJsonContext.cs | JSON source generator for NativeAOT serialization |
Performance
| Metric | Value |
|---|---|
| Command latency (FFI) | ~1-5ms (direct DLL call, no process spawn) |
| Command latency (EXE CLI) | ~50-200ms (process startup overhead) |
| Session detection | Event-driven (WinRT SessionsChanged callback) |
| Timeline updates | Event-driven (TimelinePropertiesChanged, 200ms throttle) |
| Memory | Single DLL loaded once; ~5MB resident |
Testing
# Build the DLL first
npm run build:ctypes
# Run Python test
npm run test:ctypes # python test/smtc_ctypes_test.pyChangelog
4f6b6-on

