feat: improved color-coded logging + test-mode
This commit is contained in:
parent
9e37d72071
commit
b006570ab7
43
dialog.jai
43
dialog.jai
@ -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
397
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;
|
||||
|
||||
48
updater.jai
48
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user