Skip to content

Commit

Permalink
Lots and lots of debugging and shuffling code around.
Browse files Browse the repository at this point in the history
Made GUID optional as apparently it's super weird.
  • Loading branch information
simonbuchan committed Mar 20, 2019
1 parent c556655 commit 087df18
Show file tree
Hide file tree
Showing 16 changed files with 599 additions and 247 deletions.
1 change: 1 addition & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"src/data.cc",
"src/icon-object.cc",
"src/menu-object.cc",
"src/notify-icon.cc",
"src/notify-icon-object.cc",
"src/parse_guid.cc",
"src/module.cc"
Expand Down
34 changes: 21 additions & 13 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,26 @@ export namespace NotifyIcon {
* Initial properties for the clickable icon in the notification area (system tray).
*/
export interface NewOptions extends Options {
/**
* Persistent identifier for this icon.
* Must be either a 16-byte `Buffer` or a string in the standard UUID/GUID format, matching
* `/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/`,
* e.g. `"01234567-89ab-cdef-0123-456789abcdef"`.
*
* `guid` is a unique id used by Windows to preserve user preferences
* for this icon. As it is a persistent identifier, do not generate values
* for this at runtime using, e.g. the `uuid` package, generate them once
* and save it as a constant in your code.
*
* Only one icon can use the same `guid` at a time, even between processes.
*
* Be cautious about using this feature, as it reserves the guid for the
* executable path and has weird reliablility issues, but it can avoid
* duplicate settings entries for the notification icon.
*
* https://github.com/electron/electron/issues/2468
*/
guid?: string;
/**
* Automatically delete any previous icon added with the same `guid`.
* Windows only allows one instance of the icon to be added at a time,
Expand All @@ -220,24 +240,12 @@ export namespace NotifyIcon {
export class NotifyIcon {
/**
* Add a notification icon to the notification area (system tray).
*
* `guid` is a unique id used by Windows to preserve user preferences
* for this icon. As it is a persistent identifier, do not generate values
* for this at runtime using, e.g. the `uuid` package, generate them once
* and save it as a constant in your code.
*
* Only one icon can use the same `guid` at a time, even between processes.
*
* @param guid
* Persistent identifier for this icon.
* Must be either a 16-byte `Buffer` or a string in the standard UUID/GUID format, matching
* `/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/`,
* e.g. `"01234567-89ab-cdef-0123-456789abcdef"`.
* @param options
* Options controlling the creation, display of the icon, and optionally
* the notification ("toast" or "balloon").
*/
constructor(guid: string, options?: NotifyIcon.NewOptions);
constructor(options?: NotifyIcon.NewOptions);

/**
* Update the options for a notification icon, and optionally a notification.
Expand Down
102 changes: 97 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,105 @@
let bindings;
let native;
try {
bindings = require('./build/Release/tray.node');
native = require('./build/Release/tray.node');
} catch (e) {
bindings = require('./build/Debug/tray.node');
native = require('./build/Debug/tray.node');
}

module.exports = bindings;
const { NotifyIcon, Icon, Menu } = native;

const { Icon, Menu } = bindings;
module.exports = { NotifyIcon, Icon, Menu, printIconStream };

function printIconStream() {
/** @type Buffer */
const buffer = native.icon_stream;
// const headerSize = buffer.readUInt32LE(0);
// buffer.readUInt32LE(4); = 7
// buffer.readUInt16LE(8); = 1
// buffer.readUInt16LE(10); = 1
const itemCount = buffer.readUInt32LE(12);
const itemListOffset = buffer.readUInt32LE(16);

const itemSize = 1640;

const pathStart = 0;
const pathEnd = pathStart + 2 * 260;
const tipStart = pathEnd + 16;
const tipEnd = tipStart + 2 * 260; // 260 seems to be closer than the NOTIFYITEMDATA size of 128
const infoStart = tipEnd + 60;
const infoEnd = infoStart + 2 * 260;

console.log("header data = %s", buffer.slice(0, itemListOffset).toString("hex"));

for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
const itemOffset = itemListOffset + itemIndex * itemSize;
const itemData = buffer.slice(itemOffset, itemOffset + itemSize);
console.group('Item %d/%d at 0x%s', itemIndex, itemCount, itemOffset.toString(16));

const [path, pathTrailing] = readUTF16(itemData, pathStart, pathEnd);
console.log("path = %O", stringRot(path, 13));
if (pathTrailing.some(b => b != 0)) {
console.log(" trailing = %s", pathTrailing.toString("hex"));
}

console.group("? = %s", itemData.slice(pathEnd, pathEnd + 12).toString("hex"));
console.log("? = %O", itemData.readInt32LE(pathEnd));
console.log("? = %O", itemData.readInt32LE(pathEnd + 4));
console.log("? = %O", itemData.readInt32LE(pathEnd + 8));
console.groupEnd();

console.log("last seen = %d-%d", itemData.readUInt16LE(pathEnd + 12), itemData.readUInt16LE(pathEnd + 14));

const [tip, tipTrailing] = readUTF16(itemData, tipStart, tipEnd);
console.log("tooltip = %O", stringRot(tip, 13));
if (tipTrailing.some(b => b != 0)) {
console.log(" trailing = %s", tipTrailing.toString("hex"));
}
console.log("? = %O", itemData.readInt32LE(tipEnd));
console.log("? = %O", itemData.readInt32LE(tipEnd + 4));
console.log("order? = %O", itemData.readInt32LE(tipEnd + 8));
console.log("? = %s", itemData.slice(tipEnd + 12, tipEnd + 40).toString("hex"));
console.log("? = %s", itemData.slice(tipEnd + 40, infoStart).toString("hex"));

const [info, infoTrailing] = readUTF16(itemData, infoStart, infoEnd);
console.log("info? = %O", stringRot(info, 13));
console.log("? = %s", infoTrailing.slice(0, 4).toString("hex"));
const [info2, info2Trailing] = readUTF16(infoTrailing, 4);
console.log("? = %O", stringRot(info2, 13));
if (info2Trailing.some(b => b != 0)) {
console.log(" trailing = %s", info2Trailing.toString("hex"));
}

console.log("? = %O", itemData.readUInt32LE(infoEnd));
console.groupEnd();
}

console.log("trailer data = %s", buffer.slice(itemListOffset + itemCount * itemSize).toString("hex"));
}

function stringRot(value, rot) {
const codes = Array(value.length);
for (let i = 0; i != value.length; i++) {
const code = value.charCodeAt(i);
if (65 <= code && code <= 90) {
codes[i] = (code - 65 + rot) % 26 + 65;
} else if (97 <= code && code <= 122) {
codes[i] = (code - 97 + rot) % 26 + 97;
} else {
codes[i] = code;
}
}
return String.fromCharCode(...codes);
}

function readUTF16(buffer, offset = 0, bufferEnd = buffer.length) {
let end = offset;
while (end < bufferEnd && buffer.readUInt16LE(end)) {
end += 2;
}
const value = buffer.slice(offset, end).toString('utf16le');
const trailing = buffer.slice(end + 2, bufferEnd);
return [value, trailing];
}

Object.defineProperties(Menu, {
createTemplate: { value: createMenuTemplate, enumerable: true },
Expand Down
48 changes: 23 additions & 25 deletions src/data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,25 @@ napi_status EnvData::add_icon(int32_t id, napi_value value,
}
napi_ref ref;
NAPI_RETURN_IF_NOT_OK(napi_create_reference(env, value, 1, &ref));
icons.insert({id, {ref, object}});
if (icons
.insert({id,
{ref, object->notify_icon.id.callback_id,
object->notify_icon.id.guid}})
.second == false) {
NAPI_RETURN_IF_NOT_OK(napi_delete_reference(env, ref));
}
return napi_ok;
}

void EnvData::remove_icon(int32_t id) {
if (auto it = icons.find(id); it != icons.end()) {
napi_reference_unref(env, it->second.ref, nullptr);
icons.erase(id);
bool EnvData::remove_icon(int32_t id) {
if (auto it = icons.find(id); it == icons.end()) {
return false;
} else {
icons.erase(it);
if (icons.empty()) {
uv_idle_stop(&message_pump_idle);
}
}
}

EnvData::~EnvData() {
for (auto it = icons.begin(); it != icons.end(); it = icons.begin()) {
// remove will invalidate the iterator.
it->second.object->remove(env);
return true;
}
}

Expand All @@ -82,8 +83,10 @@ static napi_status notify_select(EnvData* env_data, int32_t icon_id,
napi_value value;
NAPI_RETURN_IF_NOT_OK(
napi_get_reference_value(env, it->second.ref, &value));
NotifyIconObject* object;
NAPI_RETURN_IF_NOT_OK(napi_get_value(env, value, &object));
NAPI_RETURN_IF_NOT_OK(
it->second.object->select(env, value, right_button, mouse_x, mouse_y));
object->select(env, value, right_button, mouse_x, mouse_y));
}
return napi_ok;
}
Expand Down Expand Up @@ -126,17 +129,6 @@ LRESULT messageWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
break;
}
}
// case WM_MENUSELECT: {
// auto menu = (HMENU)lParam;
// auto flags = HIWORD(wParam);
// if (flags & MF_POPUP) {
// auto env = (napi_env)(void*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
// int32_t item_id = LOWORD(wParam);
// if (auto icon_data = get_icon_data_by_menu(env, menu, item_id);
// icon_data && icon_data->select_callback) {
// }
// }
// }

return DefWindowProc(hwnd, msg, wParam, lParam);
}
Expand Down Expand Up @@ -218,4 +210,10 @@ std::tuple<napi_status, EnvData*> create_env_data(napi_env env) {
data->msg_hwnd = hwnd;

return {napi_ok, data};
}
}

EnvData::~EnvData() {
for (auto& pair : icons) {
delete_notify_icon({msg_hwnd, pair.second.id, pair.second.guid});
}
}
5 changes: 3 additions & 2 deletions src/data.hh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ struct NotifyIconObject;
struct EnvData {
struct IconData {
napi_ref ref;
NotifyIconObject* object;
int32_t id;
std::optional<GUID> guid;
};

// Note that there's not much point trying to clean up these on
Expand All @@ -35,7 +36,7 @@ struct EnvData {
uv_idle_t message_pump_idle = {this};

napi_status add_icon(int32_t id, napi_value value, NotifyIconObject* object);
void remove_icon(int32_t id);
bool remove_icon(int32_t id);

~EnvData();
};
Expand Down
6 changes: 6 additions & 0 deletions src/menu-object.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ napi_value export_Menu_showSync(napi_env env, napi_callback_info info) {
menu,
GetSystemMetrics(SM_MENUDROPALIGNMENT) | TPM_RETURNCMD | TPM_NONOTIFY,
mouse_x, mouse_y, env_data->msg_hwnd, nullptr);
if (!item_id) {
if (auto code = GetLastError(); code) {
napi_throw_win32_error(env, "TrackPopupMenuEx", (HRESULT) code);
return nullptr;
}
}

napi_value result;
NAPI_THROW_RETURN_NULL_IF_NOT_OK(env, napi_create(env, item_id, &result));
Expand Down
66 changes: 66 additions & 0 deletions src/module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@
#include "napi/napi.hh"
#include "notify-icon-object.hh"

// https://gist.github.com/paulcbetts/c4e3412262324b551dda
struct IconStreams {
struct item_t {
wchar_t exe_path[MAX_PATH];
int32_t unknown1;
int32_t unknown2;
};

int32_t header_size;
int32_t unknown1;
int16_t unknown2;
int16_t unknown3;
int32_t count;
int32_t items_offset;

item_t items[1];
};

constexpr auto icon_stream_key =
L"Software\\Classes\\Local Settings\\Software\\"
L"Microsoft\\Windows\\CurrentVersion\\TrayNotify";
constexpr auto icon_stream_value = L"IconStreams";

napi_value get_icon_stream(napi_env env, napi_callback_info info) {
DWORD size = 0;
if (auto error =
RegGetValueW(HKEY_CURRENT_USER, icon_stream_key, icon_stream_value,
RRF_RT_REG_BINARY, nullptr, nullptr, &size);
error) {
napi_throw_win32_error(env, "RegGetValueW", error);
return nullptr;
}

void* data = nullptr;
napi_value result = nullptr;
NAPI_THROW_RETURN_NULL_IF_NOT_OK(
env, napi_create_buffer(env, size, &data, &result));

if (auto error =
RegGetValueW(HKEY_CURRENT_USER, icon_stream_key, icon_stream_value,
RRF_RT_REG_BINARY, nullptr, data, &size);
error) {
napi_throw_win32_error(env, "RegGetValueW", error);
return nullptr;
}

return result;
}

napi_value set_icon_stream(napi_env env, napi_callback_info info) {
napi_buffer_info value;
NAPI_RETURN_NULL_IF_NOT_OK(napi_get_required_args(env, info, &value));

if (auto error =
RegSetKeyValueW(HKEY_CURRENT_USER, icon_stream_key, icon_stream_value,
REG_BINARY, value.data, value.size);
error) {
napi_throw_win32_error(env, "RegSetKeyValueW", error);
return nullptr;
}

return nullptr;
}

NAPI_MODULE_INIT() {
EnvData* env_data;
if (auto [status, new_env_data] = create_env_data(env); status != napi_ok) {
Expand All @@ -28,6 +92,8 @@ NAPI_MODULE_INIT() {
env, napi_define_properties(
env, exports,
{
napi_getter_setter_property("icon_stream", get_icon_stream,
set_icon_stream),
napi_value_property("NotifyIcon", notify_icon_constructor),
napi_value_property("Menu", menu_constructor),
napi_value_property("Icon", icon_constructor),
Expand Down
13 changes: 13 additions & 0 deletions src/napi/props.hh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ inline napi_property_descriptor napi_getter_property(
return desc;
}

inline napi_property_descriptor napi_getter_setter_property(
const char *utf8name, napi_callback getter, napi_callback setter,
napi_property_attributes attributes = napi_enumerable,
void *data = nullptr) {
napi_property_descriptor desc = {};
desc.utf8name = utf8name;
desc.getter = getter;
desc.setter = setter;
desc.attributes = attributes;
desc.data = data;
return desc;
}

inline napi_property_descriptor napi_method_property(
const char *utf8name, napi_callback method,
napi_property_attributes attributes = napi_enumerable,
Expand Down
Loading

0 comments on commit 087df18

Please sign in to comment.