feat: improved color-coded logging + test-mode

This commit is contained in:
Ixniy Evonniy 2026-07-01 00:46:03 +03:00
parent 9e37d72071
commit b006570ab7
5 changed files with 392 additions and 100 deletions

BIN
cache.db

Binary file not shown.

View File

@ -12,6 +12,7 @@ Dialog_State :: struct {
cancel_hwnd: HWND;
dialog_done: bool;
url_saved: bool;
is_test_mode: bool;
}
dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
@ -53,8 +54,9 @@ dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT
);
SendMessageW(state.edit_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
// Populate with existing URL if present from config.json
config_data, read_ok := read_entire_file("config.json");
// Populate with existing URL if present
config_filename := ifx state.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
existing_url := get_json_string_field(config_data, "\"_url\"");
if existing_url {
@ -108,7 +110,8 @@ dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT
trimmed_url := trim(url_utf8);
if trimmed_url {
mode: string;
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx state.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
extracted_mode := get_json_string_field(config_data, "\"_mode\"");
if extracted_mode {
@ -123,11 +126,12 @@ dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT
defer free(mode);
minimal_json := tprint("{\n \"_url\": \"%\",\n \"_mode\": \"%\",\n \"inbounds\": [],\n \"outbounds\": []\n}", trimmed_url, mode);
write_entire_file("config.json", minimal_json);
write_entire_file(config_filename, minimal_json);
state.url_saved = true;
}
} else {
DeleteFileW(utf8_to_wide("config.json"));
config_filename := ifx state.is_test_mode then "config_test.json" else "config.json";
DeleteFileW(utf8_to_wide(config_filename));
state.url_saved = true;
}
DestroyWindow(hwnd);
@ -153,7 +157,7 @@ dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT
}
}
show_config_url_dialog :: (parent_hwnd: HWND) -> bool {
show_config_url_dialog :: (parent_hwnd: HWND, is_test_mode := false) -> bool {
hInstance := GetModuleHandleW(null);
class_name := utf8_to_wide("SingboxConfigUrlDialogClass");
@ -177,6 +181,7 @@ show_config_url_dialog :: (parent_hwnd: HWND) -> bool {
dialog_y := (screen_h - dialog_h) / 2;
state: Dialog_State;
state.is_test_mode = is_test_mode;
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,
@ -220,7 +225,7 @@ show_config_url_dialog :: (parent_hwnd: HWND) -> bool {
return state.url_saved;
}
show_config_port_dialog :: (parent_hwnd: HWND) -> bool {
show_config_port_dialog :: (parent_hwnd: HWND, is_test_mode := false) -> bool {
// Port Dialog Proc
port_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
ctx: #Context;
@ -262,9 +267,19 @@ show_config_port_dialog :: (parent_hwnd: HWND) -> bool {
SendMessageW(state.edit_hwnd, WM_SETFONT, cast(WPARAM) hFont, TRUE);
// Populate with existing port if present
config_data, read_ok := read_entire_file("config.json");
port := 10801;
if read_ok {
config_filename := ifx state.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
port := ifx state.is_test_mode then 10899 else 10801;
if !state.is_test_mode && read_ok {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok {
port = xx val;
}
}
free(config_data);
} else if state.is_test_mode && read_ok {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
@ -323,7 +338,8 @@ show_config_port_dialog :: (parent_hwnd: HWND) -> bool {
if ok && val > 0 && val <= 65535 {
port := xx val;
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx state.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
url := "";
mode := "system_proxy";
if read_ok {
@ -338,12 +354,12 @@ show_config_port_dialog :: (parent_hwnd: HWND) -> bool {
}
updated := set_json_metadata(config_data, url, mode, port);
write_entire_file("config.json", updated);
write_entire_file(config_filename, updated);
free(config_data);
free(updated);
} else {
minimal_json := tprint("{\n \"_url\": \"\",\n \"_mode\": \"system_proxy\",\n \"_port\": %,\n \"inbounds\": [],\n \"outbounds\": []\n}", port);
write_entire_file("config.json", minimal_json);
write_entire_file(config_filename, minimal_json);
}
if url free(url);
free(mode);
@ -393,6 +409,7 @@ show_config_port_dialog :: (parent_hwnd: HWND) -> bool {
dialog_y := (screen_h - dialog_h) / 2;
state: Dialog_State;
state.is_test_mode = is_test_mode;
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,

397
main.jai
View File

@ -15,6 +15,11 @@ TIMER_ANIMATION :: 2;
IDI_APPLICATION :: cast(*u16) 32512;
IDI_SHIELD :: cast(*u16) 32518;
Log_Reader_Data :: struct {
pipe_read_handle: HANDLE;
is_test_mode: bool;
}
App_State :: struct {
hwnd: HWND;
singbox_running: bool;
@ -35,12 +40,186 @@ App_State :: struct {
is_updating: bool;
animation_frame: int;
icon_frames: [4] HICON;
is_test_mode: bool;
log_reader_thread: Thread;
log_reader_data: Log_Reader_Data;
log_reader_started: bool;
}
// Custom log print sending logs to OutputDebugStringW
Log_Level :: enum {
INFO;
WARN;
ERROR;
DEBUG;
}
append_to_log_file :: (text: string) {
OPEN_ALWAYS :: 4;
FILE_END :: 2;
filename := ifx global_is_test_mode then "singbox_tray_test.log" else "singbox_tray.log";
wide_path := utf8_to_wide(filename);
hFile := CreateFileW(
wide_path,
GENERIC_WRITE,
FILE_SHARE_READ,
null,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
null
);
if hFile != INVALID_HANDLE_VALUE {
SetFilePointer(hFile, 0, null, FILE_END);
bytes_written: DWORD;
WriteFile(hFile, text.data, xx text.count, *bytes_written, null);
CloseHandle(hFile);
}
}
log_write :: (level: Log_Level, format_string: string, args: .. Any) {
st: SYSTEMTIME;
GetLocalTime(*st);
time_str := tprint("[%-%-% %:%:%.%]",
formatInt(st.wYear, minimum_digits = 4),
formatInt(st.wMonth, minimum_digits = 2),
formatInt(st.wDay, minimum_digits = 2),
formatInt(st.wHour, minimum_digits = 2),
formatInt(st.wMinute, minimum_digits = 2),
formatInt(st.wSecond, minimum_digits = 2),
formatInt(st.wMilliseconds, minimum_digits = 3)
);
level_str := "";
color_prefix := "";
if level == {
case .INFO;
level_str = "[TRAY] [INFO]";
color_prefix = "\x1b[32m"; // Green
case .WARN;
level_str = "[TRAY] [WARN]";
color_prefix = "\x1b[33m"; // Yellow
case .ERROR;
level_str = "[TRAY] [ERROR]";
color_prefix = "\x1b[31m"; // Red
case .DEBUG;
level_str = "[TRAY] [DEBUG]";
color_prefix = "\x1b[36m"; // Cyan
}
message := tprint(format_string, .. args);
while message.count > 0 && (message[message.count - 1] == #char "\n" || message[message.count - 1] == #char "\r") {
message.count -= 1;
}
// Colored format for log viewers that support ANSI escape codes
// \x1b[90m is Dark Grey/Dim for timestamps, \x1b[0m resets color
colored_log := tprint("\x1b[90m%\x1b[0m %%\x1b[0m %\n", time_str, color_prefix, level_str, message);
// Write to OutputDebugStringW (useful for live debugging in VS / DebugView)
OutputDebugStringW(utf8_to_wide(colored_log));
// Append to singbox_tray.log file
append_to_log_file(colored_log);
}
log_singbox :: (message: string, is_test_mode: bool) {
st: SYSTEMTIME;
GetLocalTime(*st);
time_str := tprint("[%-%-% %:%:%.%]",
formatInt(st.wYear, minimum_digits = 4),
formatInt(st.wMonth, minimum_digits = 2),
formatInt(st.wDay, minimum_digits = 2),
formatInt(st.wHour, minimum_digits = 2),
formatInt(st.wMinute, minimum_digits = 2),
formatInt(st.wSecond, minimum_digits = 2),
formatInt(st.wMilliseconds, minimum_digits = 3)
);
// [SING-BOX] tag in Magenta (\x1b[35m)
colored_log := tprint("\x1b[90m%\x1b[0m \x1b[35m[SING-BOX]\x1b[0m %\n", time_str, message);
OutputDebugStringW(utf8_to_wide(colored_log));
// Append to the correct log file
filename := ifx is_test_mode then "singbox_tray_test.log" else "singbox_tray.log";
wide_path := utf8_to_wide(filename);
hFile := CreateFileW(
wide_path,
GENERIC_WRITE,
FILE_SHARE_READ,
null,
4, // OPEN_ALWAYS
0x80, // FILE_ATTRIBUTE_NORMAL
null
);
if hFile != INVALID_HANDLE_VALUE {
SetFilePointer(hFile, 0, null, 2); // FILE_END
bytes_written: DWORD;
WriteFile(hFile, colored_log.data, xx colored_log.count, *bytes_written, null);
CloseHandle(hFile);
}
}
log_reader_thread_proc :: (thread: *Thread) -> s64 {
data := cast(*Log_Reader_Data) thread.data;
if !data return 1;
buffer: [1024] u8 = ---;
bytes_read: DWORD;
line_builder: String_Builder;
defer free_buffers(*line_builder);
while true {
ok := ReadFile(data.pipe_read_handle, buffer.data, xx buffer.count, *bytes_read, null);
if !ok || bytes_read == 0 {
break;
}
for i: 0..bytes_read-1 {
char := buffer[i];
if char == #char "\n" {
line := builder_to_string(*line_builder);
// strip trailing \r if present
if line.count > 0 && line[line.count - 1] == #char "\r" {
line.count -= 1;
}
log_singbox(line, data.is_test_mode);
free(line);
} else {
append(*line_builder, char);
}
}
}
// Process any remaining bytes
remaining := builder_to_string(*line_builder);
if remaining.count > 0 {
if remaining[remaining.count - 1] == #char "\r" {
remaining.count -= 1;
}
log_singbox(remaining, data.is_test_mode);
}
free(remaining);
CloseHandle(data.pipe_read_handle);
return 0;
}
log_info :: (format_string: string, args: .. Any) { log_write(.INFO, format_string, ..args); }
log_warn :: (format_string: string, args: .. Any) { log_write(.WARN, format_string, ..args); }
log_error :: (format_string: string, args: .. Any) { log_write(.ERROR, format_string, ..args); }
log_debug :: (format_string: string, args: .. Any) { log_write(.DEBUG, format_string, ..args); }
log_print :: (format_string: string, args: .. Any) {
formatted := tprint(format_string, .. args);
OutputDebugStringW(utf8_to_wide(formatted));
log_info(format_string, ..args);
}
file_exists :: (path: string) -> bool {
@ -51,37 +230,16 @@ file_exists :: (path: string) -> bool {
}
// Hidden Process Spawning with Job Object configuration and stdout/stderr redirection
create_process_hidden :: (cmd: string, log_file: string = "") -> HANDLE, PROCESS_INFORMATION, bool {
create_process_hidden :: (cmd: string, stdout_handle: HANDLE = INVALID_HANDLE_VALUE) -> HANDLE, PROCESS_INFORMATION, bool {
startup_info: STARTUPINFOW;
startup_info.cb = size_of(STARTUPINFOW);
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
file_handle: HANDLE = INVALID_HANDLE_VALUE;
if log_file {
sa: SECURITY_ATTRIBUTES;
sa.nLength = size_of(SECURITY_ATTRIBUTES);
sa.bInheritHandle = 1; // TRUE
sa.lpSecurityDescriptor = null;
wide_log := utf8_to_wide(log_file);
file_handle = CreateFileW(
wide_log,
GENERIC_WRITE,
FILE_SHARE_READ,
*sa,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
null
);
if file_handle != INVALID_HANDLE_VALUE {
startup_info.dwFlags |= STARTF_USESTDHANDLES;
startup_info.hStdOutput = file_handle;
startup_info.hStdError = file_handle;
}
if stdout_handle != INVALID_HANDLE_VALUE {
startup_info.dwFlags |= STARTF_USESTDHANDLES;
startup_info.hStdOutput = stdout_handle;
startup_info.hStdError = stdout_handle;
}
process_info: PROCESS_INFORMATION;
@ -97,7 +255,7 @@ create_process_hidden :: (cmd: string, log_file: string = "") -> HANDLE, PROCESS
cmd_wide := utf8_to_wide(cmd);
inherit_handles := ifx file_handle != INVALID_HANDLE_VALUE then cast(BOOL) 1 else cast(BOOL) 0;
inherit_handles := ifx stdout_handle != INVALID_HANDLE_VALUE then cast(BOOL) 1 else cast(BOOL) 0;
success := CreateProcessW(
null,
@ -116,10 +274,6 @@ create_process_hidden :: (cmd: string, log_file: string = "") -> HANDLE, PROCESS
AssignProcessToJobObject(job_handle, process_info.hProcess);
}
if file_handle != INVALID_HANDLE_VALUE {
CloseHandle(file_handle);
}
return job_handle, process_info, cast(bool) success;
}
@ -160,12 +314,16 @@ update_tray :: (app: *App_State) {
start_singbox :: (app: *App_State) -> bool {
if app.singbox_running return true;
has_config := file_exists("config.json");
config_filename := ifx app.is_test_mode then "config_test.json" else "config.json";
config_run_filename := ifx app.is_test_mode then "config_run_test.json" else "config_run.json";
singbox_log_filename := ifx app.is_test_mode then "sing-box_test.log" else "sing-box.log";
has_config := file_exists(config_filename);
url_present := false;
has_outbounds := false;
if has_config {
config_data, read_ok := read_entire_file("config.json");
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
url := get_json_string_field(config_data, "\"_url\"");
if url {
@ -180,7 +338,7 @@ start_singbox :: (app: *App_State) -> bool {
if !has_config || !has_outbounds {
if url_present {
changed, success, err_msg := perform_update();
changed, success, err_msg := perform_update(app.is_test_mode);
if !success {
msg := tprint("Could not download configuration.\nError: %", err_msg);
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
@ -192,8 +350,8 @@ start_singbox :: (app: *App_State) -> bool {
}
}
if file_exists("config.json") {
config_data, read_ok := read_entire_file("config.json");
if file_exists(config_filename) {
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
// Modify inbounds in memory with the custom port
modified := modify_config_inbounds(config_data, app.port);
@ -201,8 +359,8 @@ start_singbox :: (app: *App_State) -> bool {
// Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error
clean_config := strip_json_metadata(modified);
write_entire_file("config_run.json", clean_config);
log_print("Generated clean config_run.json for sing-box (port %).\n", app.port);
write_entire_file(config_run_filename, clean_config);
log_print("Generated clean % for sing-box (port %).\n", config_run_filename, app.port);
free(config_data);
free(modified);
@ -218,9 +376,29 @@ start_singbox :: (app: *App_State) -> bool {
}
}
cmd := tprint("% run -c config_run.json", exe_path);
job, pi, ok := create_process_hidden(cmd, "sing-box.log");
hReadPipe, hWritePipe: HANDLE;
sa: SECURITY_ATTRIBUTES;
sa.nLength = size_of(SECURITY_ATTRIBUTES);
sa.bInheritHandle = 1; // TRUE
sa.lpSecurityDescriptor = null;
if !CreatePipe(*hReadPipe, *hWritePipe, *sa, 0) {
log_error("Failed to create pipe for sing-box output redirection.\n");
hWritePipe = INVALID_HANDLE_VALUE;
hReadPipe = INVALID_HANDLE_VALUE;
} else {
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
}
cmd := tprint("% run -c %", exe_path, config_run_filename);
job, pi, ok := create_process_hidden(cmd, hWritePipe);
if hWritePipe != INVALID_HANDLE_VALUE {
CloseHandle(hWritePipe);
}
if !ok {
if hReadPipe != INVALID_HANDLE_VALUE CloseHandle(hReadPipe);
msg := tprint("Failed to start %. Please ensure sing-box.exe is in the same folder or in the 'sing-box' subfolder.", exe_path);
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
return false;
@ -231,14 +409,26 @@ start_singbox :: (app: *App_State) -> bool {
app.singbox_job_handle = job;
app.singbox_running = true;
if hReadPipe != INVALID_HANDLE_VALUE {
app.log_reader_data.pipe_read_handle = hReadPipe;
app.log_reader_data.is_test_mode = app.is_test_mode;
thread_init(*app.log_reader_thread, log_reader_thread_proc);
app.log_reader_thread.data = *app.log_reader_data;
app.log_reader_started = true;
thread_start(*app.log_reader_thread);
}
app.is_connecting = true;
app.connecting_ticks = 0;
app.animation_frame = 0;
SetTimer(app.hwnd, TIMER_ANIMATION, 250, null);
if app.use_system_proxy {
if app.use_system_proxy && !app.is_test_mode {
set_windows_system_proxy(true, tprint("127.0.0.1:%", app.port));
log_print("System proxy enabled on port %.\n", app.port);
} else if app.is_test_mode {
log_info("Test Mode: System proxy configuration bypassed.\n");
}
update_tray(app);
@ -262,11 +452,22 @@ stop_singbox :: (app: *App_State) {
app.singbox_job_handle = null;
app.singbox_running = false;
set_windows_system_proxy(false, "");
log_print("System proxy disabled.\n");
if app.log_reader_started {
thread_is_done(*app.log_reader_thread, -1);
thread_deinit(*app.log_reader_thread);
app.log_reader_started = false;
}
if !app.is_test_mode {
set_windows_system_proxy(false, "");
log_print("System proxy disabled.\n");
} else {
log_info("Test Mode: System proxy bypass kept on stop.\n");
}
// Clean up temporary run config
DeleteFileW(utf8_to_wide("config_run.json"));
config_run_filename := ifx app.is_test_mode then "config_run_test.json" else "config_run.json";
DeleteFileW(utf8_to_wide(config_run_filename));
update_tray(app);
log_print("Sing-box stopped.\n");
@ -279,7 +480,7 @@ check_process_status :: (app: *App_State) {
success := GetExitCodeProcess(app.singbox_process_handle, *exit_code);
STILL_ACTIVE :: 259;
if success && exit_code != STILL_ACTIVE {
log_print("Sing-box process exited unexpectedly with code %.\n", exit_code);
log_error("Sing-box process exited unexpectedly with code %.\n", exit_code);
CloseHandle(app.singbox_process_handle);
CloseHandle(app.singbox_thread_handle);
@ -290,10 +491,21 @@ check_process_status :: (app: *App_State) {
app.singbox_job_handle = null;
app.singbox_running = false;
set_windows_system_proxy(false, "");
log_print("System proxy disabled due to unexpected exit.\n");
if app.log_reader_started {
thread_is_done(*app.log_reader_thread, -1);
thread_deinit(*app.log_reader_thread);
app.log_reader_started = false;
}
DeleteFileW(utf8_to_wide("config_run.json"));
if !app.is_test_mode {
set_windows_system_proxy(false, "");
log_warn("System proxy disabled due to unexpected exit.\n");
} else {
log_info("Test Mode: System proxy bypass kept on unexpected exit.\n");
}
config_run_filename := ifx app.is_test_mode then "config_run_test.json" else "config_run.json";
DeleteFileW(utf8_to_wide(config_run_filename));
update_tray(app);
}
@ -305,7 +517,7 @@ trigger_immediate_update :: (app: *App_State) {
SetTimer(app.hwnd, TIMER_ANIMATION, 150, null);
update_tray(app);
changed, success, err_msg := perform_update();
changed, success, err_msg := perform_update(app.is_test_mode);
app.is_updating = false;
@ -386,18 +598,19 @@ show_context_menu :: (app: *App_State) {
}
}
case CMD_SET_URL; {
changed := show_config_url_dialog(app.hwnd);
changed := show_config_url_dialog(app.hwnd, app.is_test_mode);
if changed {
log_print("URL configured, triggering download...\n");
trigger_immediate_update(app);
}
}
case CMD_SET_PORT; {
changed := show_config_port_dialog(app.hwnd);
changed := show_config_port_dialog(app.hwnd, app.is_test_mode);
if changed {
log_print("Port configured, updating state...\n");
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx app.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
@ -423,19 +636,22 @@ show_context_menu :: (app: *App_State) {
if app.use_system_proxy {
app.use_system_proxy = false;
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx app.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
url := get_json_string_field(config_data, "\"_url\"");
updated := set_json_metadata(config_data, url, "proxy", app.port);
write_entire_file("config.json", updated);
write_entire_file(config_filename, updated);
free(config_data);
free(updated);
}
log_print("Switched to Proxy Mode.\n");
if app.singbox_running {
set_windows_system_proxy(false, "");
log_print("System proxy disabled.\n");
if !app.is_test_mode {
set_windows_system_proxy(false, "");
log_print("System proxy disabled.\n");
}
stop_singbox(app);
start_singbox(app);
}
@ -445,11 +661,12 @@ show_context_menu :: (app: *App_State) {
if !app.use_system_proxy {
app.use_system_proxy = true;
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx app.is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
url := get_json_string_field(config_data, "\"_url\"");
updated := set_json_metadata(config_data, url, "system_proxy", app.port);
write_entire_file("config.json", updated);
write_entire_file(config_filename, updated);
free(config_data);
free(updated);
}
@ -537,9 +754,22 @@ load_stock_icon :: (siid: s32) -> HICON {
return LoadIconW(null, IDI_APPLICATION);
}
global_is_test_mode: bool = false;
main :: () {
args := get_command_line_arguments();
is_test := false;
for args {
if it == "-test" || it == "--test" {
is_test = true;
break;
}
}
global_is_test_mode = is_test;
hInstance := GetModuleHandleW(null);
class_name := utf8_to_wide("SingboxTrayControllerClass");
class_name_str := ifx is_test then "SingboxTrayControllerClassTest" else "SingboxTrayControllerClass";
class_name := utf8_to_wide(class_name_str);
wclass: WNDCLASSEXW;
wclass.cbSize = size_of(WNDCLASSEXW);
@ -552,6 +782,7 @@ main :: () {
defer UnregisterClassW(class_name, hInstance);
app_state: App_State;
app_state.is_test_mode = is_test;
app_state.icon_running = load_stock_icon(SIID_WORLD);
app_state.icon_stopped = load_stock_icon(SIID_ERROR);
app_state.icon_frames[0] = load_stock_icon(SIID_SERVER);
@ -559,10 +790,11 @@ main :: () {
app_state.icon_frames[2] = load_stock_icon(SIID_WORLD);
app_state.icon_frames[3] = load_stock_icon(SIID_INTERNET);
title_str := ifx is_test then "Singbox Tray Controller (Test Mode)" else "Singbox Tray Controller";
hwnd := CreateWindowExW(
0,
class_name,
utf8_to_wide("Singbox Tray Controller"),
utf8_to_wide(title_str),
0,
0, 0, 0, 0,
null,
@ -572,30 +804,42 @@ main :: () {
);
if !hwnd {
log_print("Failed to create hidden window!\n");
log_error("Failed to create hidden window!\n");
return;
}
app_state.hwnd = hwnd;
// Load persisted mode and port from config.json
config_data, read_ok := read_entire_file("config.json");
config_filename := ifx app_state.is_test_mode then "config_test.json" else "config.json";
// Load persisted mode and port from config file
config_data, read_ok := read_entire_file(config_filename);
if read_ok {
extracted_mode := get_json_string_field(config_data, "\"_mode\"");
if extracted_mode && trim(extracted_mode) == "proxy" {
app_state.use_system_proxy = false;
}
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok {
app_state.port = xx val;
log_print("Loaded port: %\n", app_state.port);
if app_state.is_test_mode {
app_state.port = 10899; // force test port in test mode
log_info("Loaded config from %. Test mode: forcing port to %.\n", config_filename, app_state.port);
} else {
port_str := get_json_val_field(config_data, "\"_port\"");
if port_str {
val, ok := to_integer(port_str);
if ok {
app_state.port = xx val;
log_print("Loaded port: %\n", app_state.port);
}
}
}
free(config_data);
} else {
if app_state.is_test_mode {
app_state.port = 10899;
log_info("No % found. Initialized with test port %.\n", config_filename, app_state.port);
}
}
nid: NOTIFYICONDATAW;
@ -605,15 +849,17 @@ main :: () {
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_TRAY_CALLBACK;
nid.hIcon = app_state.icon_stopped;
set_tray_tip(*nid, "Sing-box: Stopped");
tip_prefix := ifx app_state.is_test_mode then "Sing-box (Test Mode)" else "Sing-box";
set_tray_tip(*nid, tprint("%: Stopped", tip_prefix));
if !Shell_NotifyIconW(NIM_ADD, *nid) {
log_print("Failed to register tray icon!\n");
log_error("Failed to register tray icon!\n");
return;
}
defer Shell_NotifyIconW(NIM_DELETE, *nid);
if file_exists("config.json") {
if file_exists(config_filename) {
start_singbox(*app_state);
}
@ -623,6 +869,7 @@ main :: () {
app_state.updater_data.hwnd = hwnd;
app_state.updater_data.update_interval_seconds = 3600;
app_state.updater_data.stop_event = CreateEventW(null, TRUE, FALSE, null);
app_state.updater_data.is_test_mode = app_state.is_test_mode;
thread_init(*app_state.updater_thread, updater_thread_proc);
app_state.updater_thread.data = *app_state.updater_data;

View File

@ -9,9 +9,12 @@ Updater_Thread_Data :: struct {
hwnd: HWND;
update_interval_seconds: s32 = 3600; // 1 hour
stop_event: HANDLE;
is_test_mode: bool;
}
download_url :: (url: string) -> string, bool, string {
log_info("Downloading config from URL: %\n", url);
hInternet := InternetOpenW(
utf8_to_wide("SingboxTrayUpdater"),
INTERNET_OPEN_TYPE_PRECONFIG,
@ -72,23 +75,35 @@ download_url :: (url: string) -> string, bool, string {
append(*builder, buffer.data, xx bytes_read);
}
return builder_to_string(*builder), true, "";
result_str := builder_to_string(*builder);
log_info("Configuration downloaded successfully (% bytes).\n", result_str.count);
return result_str, true, "";
}
perform_update :: () -> changed: bool, success: bool, error_msg: string {
config_data, read_ok := read_entire_file("config.json");
if !read_ok return false, false, "config.json does not exist. Please configure the Config URL first.";
perform_update :: (is_test_mode := false) -> changed: bool, success: bool, error_msg: string {
config_filename := ifx is_test_mode then "config_test.json" else "config.json";
config_data, read_ok := read_entire_file(config_filename);
if !read_ok {
err := tprint("% does not exist. Please configure the Config URL first.", config_filename);
return false, false, err;
}
defer free(config_data);
url := get_json_string_field(config_data, "\"_url\"");
if !url return false, false, "No Config URL configured in config.json. Please configure the Config URL first.";
if !url {
err := tprint("No Config URL configured in %. Please configure the Config URL first.", config_filename);
return false, false, err;
}
mode := get_json_string_field(config_data, "\"_mode\"");
if !mode mode = "system_proxy";
port_str := get_json_val_field(config_data, "\"_port\"");
port := 10801;
if port_str {
if is_test_mode {
port = 10899; // force test port
} else if port_str {
val, parse_ok, remainder := to_integer(port_str);
if parse_ok {
port = xx val;
@ -109,9 +124,10 @@ perform_update :: () -> changed: bool, success: bool, error_msg: string {
return false, true, "";
}
write_ok := write_entire_file("config.json", final_config);
write_ok := write_entire_file(config_filename, final_config);
if !write_ok {
return false, false, "Failed to write updated content to config.json";
err := tprint("Failed to write updated content to %", config_filename);
return false, false, err;
}
return true, true, "";
@ -129,11 +145,19 @@ updater_thread_proc :: (thread: *Thread) -> s64 {
break;
}
changed, success, err_msg := perform_update();
if success && changed {
if data.hwnd {
PostMessageW(data.hwnd, WM_RESTART_SINGBOX, 0, 0);
log_debug("Background update: Starting automatic config check...\n");
changed, success, err_msg := perform_update(data.is_test_mode);
if success {
if changed {
log_info("Background update: Configuration updated successfully. Triggering sing-box restart...\n");
if data.hwnd {
PostMessageW(data.hwnd, WM_RESTART_SINGBOX, 0, 0);
}
} else {
log_debug("Background update: Configuration is already up-to-date.\n");
}
} else {
log_error("Background update failed: %\n", err_msg);
}
}

View File

@ -261,3 +261,7 @@ set_windows_system_proxy :: (enable: bool, server: string) -> bool {