From b006570ab7c7f697b1e23f7eed932c2910e83ec5 Mon Sep 17 00:00:00 2001 From: Ixniy Evonniy Date: Wed, 1 Jul 2026 00:46:03 +0300 Subject: [PATCH] feat: improved color-coded logging + test-mode --- cache.db | Bin 32768 -> 32768 bytes dialog.jai | 43 ++++-- main.jai | 397 ++++++++++++++++++++++++++++++++++++++++++---------- updater.jai | 48 +++++-- win32.jai | 4 + 5 files changed, 392 insertions(+), 100 deletions(-) diff --git a/cache.db b/cache.db index ad97aac8ec5232c8cd058098ef98191ec339c5b8..51b4a128aba5fed060c4a03addfa81cdd04bb908 100644 GIT binary patch delta 58 zcmZo@U}|V!n&2Ry!2kh_GL}v+FBk}K7F5{4KS{trKocsVwqGuEW$JY~u*4#T1OSXC B4u1du delta 58 zcmZo@U}|V!n&2Ry!T 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, diff --git a/main.jai b/main.jai index c54f904..d3a2b59 100644 --- a/main.jai +++ b/main.jai @@ -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; diff --git a/updater.jai b/updater.jai index f5a8615..f9f6a40 100644 --- a/updater.jai +++ b/updater.jai @@ -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); } } diff --git a/win32.jai b/win32.jai index 3c236af..0c6e1d8 100644 --- a/win32.jai +++ b/win32.jai @@ -261,3 +261,7 @@ set_windows_system_proxy :: (enable: bool, server: string) -> bool { + + + +