1797 lines
63 KiB
Plaintext
1797 lines
63 KiB
Plaintext
#import "Basic";
|
|
#import "Windows";
|
|
#import "Windows_Utf8";
|
|
#import "Thread";
|
|
#import "String";
|
|
|
|
#load "win32.jai";
|
|
#load "dialog.jai";
|
|
#load "updater.jai";
|
|
|
|
global_userspace_dir: string;
|
|
global_config_path: string;
|
|
global_config_run_path: string;
|
|
global_log_path: string;
|
|
|
|
global_singbox_log_mutex: CRITICAL_SECTION;
|
|
global_last_singbox_lines: [8] string;
|
|
global_last_singbox_lines_count: int = 0;
|
|
|
|
|
|
init_userspace_paths :: (is_test: bool) {
|
|
name_wide := utf8_to_wide("LOCALAPPDATA");
|
|
len := GetEnvironmentVariableW(name_wide, null, 0);
|
|
if len > 0 {
|
|
buf := alloc(cast(s64) (len * 2));
|
|
defer free(buf);
|
|
GetEnvironmentVariableW(name_wide, cast(*u16) buf, len);
|
|
local_app_data := wide_to_utf8(cast(*u16) buf);
|
|
defer free(local_app_data);
|
|
|
|
global_userspace_dir = sprint("%\\sing-box-tray", local_app_data);
|
|
} else {
|
|
global_userspace_dir = copy_string(".");
|
|
}
|
|
|
|
// Ensure directory exists
|
|
dir_wide := utf8_to_wide(global_userspace_dir);
|
|
CreateDirectoryW(dir_wide, null);
|
|
|
|
config_name := ifx is_test then "config_test.json" else "config.json";
|
|
config_run_name := ifx is_test then "config_run_test.json" else "config_run.json";
|
|
log_name := ifx is_test then "sing-box-tray_test.log" else "sing-box-tray.log";
|
|
|
|
global_config_path = sprint("%\\%", global_userspace_dir, config_name);
|
|
global_config_run_path = sprint("%\\%", global_userspace_dir, config_run_name);
|
|
global_log_path = sprint("%\\%", global_userspace_dir, log_name);
|
|
|
|
// Migrate existing configuration if it exists in the executable's directory
|
|
// but not in the userspace directory yet.
|
|
if file_exists(config_name) {
|
|
if !file_exists(global_config_path) {
|
|
data, ok := read_entire_file(config_name);
|
|
if ok {
|
|
write_entire_file(global_config_path, data);
|
|
free(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !is_test {
|
|
test_config := sprint("%\\config_test.json", global_userspace_dir);
|
|
defer free(test_config);
|
|
test_config_run := sprint("%\\config_run_test.json", global_userspace_dir);
|
|
defer free(test_config_run);
|
|
test_log := sprint("%\\sing-box-tray_test.log", global_userspace_dir);
|
|
defer free(test_log);
|
|
test_singbox_log := sprint("%\\sing-box_test.log", global_userspace_dir);
|
|
defer free(test_singbox_log);
|
|
|
|
DeleteFileW(utf8_to_wide(test_config));
|
|
DeleteFileW(utf8_to_wide(test_config_run));
|
|
DeleteFileW(utf8_to_wide(test_log));
|
|
DeleteFileW(utf8_to_wide(test_singbox_log));
|
|
}
|
|
}
|
|
|
|
// Custom constants
|
|
TIMER_PROCESS_CHECK :: 1;
|
|
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;
|
|
singbox_process_handle: HANDLE;
|
|
singbox_thread_handle: HANDLE;
|
|
singbox_job_handle: HANDLE;
|
|
|
|
updater_thread: Thread;
|
|
updater_data: Updater_Thread_Data;
|
|
use_system_proxy: bool = true;
|
|
port: int = 10801;
|
|
|
|
icon_running: HICON;
|
|
icon_stopped: HICON;
|
|
|
|
is_connecting: bool;
|
|
connecting_ticks: int;
|
|
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;
|
|
|
|
local_version: string;
|
|
latest_web_version: string;
|
|
version_checker_thread: Thread;
|
|
version_checker_data: Version_Checker_Data;
|
|
}
|
|
|
|
Log_Level :: enum {
|
|
INFO;
|
|
WARN;
|
|
ERROR;
|
|
DEBUG;
|
|
}
|
|
|
|
append_to_log_file :: (text: string) {
|
|
OPEN_ALWAYS :: 4;
|
|
FILE_END :: 2;
|
|
|
|
filename := ifx global_log_path.count > 0 then global_log_path else (ifx global_is_test_mode then "sing-box-tray_test.log" else "sing-box-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 sing-box-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 global_log_path.count > 0 then global_log_path else (ifx is_test_mode then "sing-box-tray_test.log" else "sing-box-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);
|
|
}
|
|
|
|
// Store in ring buffer
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
defer LeaveCriticalSection(*global_singbox_log_mutex);
|
|
|
|
if global_last_singbox_lines_count < 8 {
|
|
global_last_singbox_lines[global_last_singbox_lines_count] = copy_string(message);
|
|
global_last_singbox_lines_count += 1;
|
|
} else {
|
|
free(global_last_singbox_lines[0]);
|
|
for i: 0..6 {
|
|
global_last_singbox_lines[i] = global_last_singbox_lines[i+1];
|
|
}
|
|
global_last_singbox_lines[7] = copy_string(message);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
log_info(format_string, ..args);
|
|
}
|
|
|
|
file_exists :: (path: string) -> bool {
|
|
INVALID_FILE_ATTRIBUTES :: 0xFFFFFFFF;
|
|
wide_path := utf8_to_wide(path);
|
|
attribs := GetFileAttributesW(wide_path);
|
|
return attribs != INVALID_FILE_ATTRIBUTES && !(attribs & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
is_autostart_enabled :: () -> bool {
|
|
hKey: HKEY;
|
|
subkey := utf8_to_wide("Software\\Microsoft\\Windows\\CurrentVersion\\Run");
|
|
status := RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, *hKey);
|
|
if status != 0 return false;
|
|
defer RegCloseKey(hKey);
|
|
|
|
value_name := utf8_to_wide("Sing-boxTray");
|
|
|
|
type: DWORD;
|
|
status = RegQueryValueExW(hKey, value_name, null, *type, null, null);
|
|
return status == 0;
|
|
}
|
|
|
|
set_autostart :: (enable: bool) -> bool {
|
|
hKey: HKEY;
|
|
subkey := utf8_to_wide("Software\\Microsoft\\Windows\\CurrentVersion\\Run");
|
|
status := RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_WRITE, *hKey);
|
|
if status != 0 return false;
|
|
defer RegCloseKey(hKey);
|
|
|
|
value_name := utf8_to_wide("Sing-boxTray");
|
|
|
|
if enable {
|
|
path_buffer: [MAX_PATH] u16;
|
|
len := GetModuleFileNameW(null, path_buffer.data, MAX_PATH);
|
|
if len == 0 return false;
|
|
|
|
path_len_bytes := cast(DWORD) ((len + 1) * 2);
|
|
status = RegSetValueExW(hKey, value_name, 0, REG_SZ, cast(*u8) path_buffer.data, path_len_bytes);
|
|
return status == 0;
|
|
} else {
|
|
status = RegDeleteValueW(hKey, value_name);
|
|
return status == 0 || status == 2; // ERROR_FILE_NOT_FOUND is 2
|
|
}
|
|
}
|
|
|
|
// Hidden Process Spawning with Job Object configuration and stdout/stderr redirection
|
|
create_process_hidden :: (cmd: string, stdout_handle: HANDLE = INVALID_HANDLE_VALUE, working_dir: string = "") -> HANDLE, PROCESS_INFORMATION, bool {
|
|
startup_info: STARTUPINFOW;
|
|
startup_info.cb = size_of(STARTUPINFOW);
|
|
startup_info.dwFlags = STARTF_USESHOWWINDOW;
|
|
startup_info.wShowWindow = SW_HIDE;
|
|
|
|
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;
|
|
|
|
CREATE_NO_WINDOW :: 0x08000000;
|
|
|
|
job_handle := CreateJobObjectA(null, null);
|
|
if job_handle {
|
|
job_info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
|
job_info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
SetInformationJobObject(job_handle, .ExtendedLimitInformation, *job_info, size_of(type_of(job_info)));
|
|
}
|
|
|
|
cmd_wide := utf8_to_wide(cmd);
|
|
|
|
inherit_handles := ifx stdout_handle != INVALID_HANDLE_VALUE then cast(BOOL) 1 else cast(BOOL) 0;
|
|
|
|
working_dir_wide: *u16 = null;
|
|
if working_dir.count > 0 {
|
|
working_dir_wide = utf8_to_wide(working_dir);
|
|
}
|
|
|
|
success := CreateProcessW(
|
|
null,
|
|
cmd_wide,
|
|
null,
|
|
null,
|
|
inherit_handles,
|
|
CREATE_NO_WINDOW,
|
|
null,
|
|
working_dir_wide,
|
|
*startup_info,
|
|
*process_info
|
|
);
|
|
|
|
if success && job_handle {
|
|
AssignProcessToJobObject(job_handle, process_info.hProcess);
|
|
}
|
|
|
|
return job_handle, process_info, cast(bool) success;
|
|
}
|
|
|
|
set_tray_tip :: (nid: *NOTIFYICONDATAW, text: string) {
|
|
wide_text := utf8_to_wide(text);
|
|
i := 0;
|
|
while wide_text[i] != 0 && i < 127 {
|
|
nid.szTip[i] = wide_text[i];
|
|
i += 1;
|
|
}
|
|
nid.szTip[i] = 0;
|
|
}
|
|
|
|
update_tray :: (app: *App_State) {
|
|
nid: NOTIFYICONDATAW;
|
|
nid.cbSize = size_of(NOTIFYICONDATAW);
|
|
nid.hWnd = app.hwnd;
|
|
nid.uID = 1;
|
|
nid.uFlags = NIF_ICON | NIF_TIP;
|
|
|
|
if app.is_connecting {
|
|
nid.hIcon = app.icon_frames[app.animation_frame];
|
|
set_tray_tip(*nid, "Sing-box: Connecting...");
|
|
} else if app.is_updating {
|
|
nid.hIcon = app.icon_frames[app.animation_frame];
|
|
set_tray_tip(*nid, "Sing-box: Updating Config...");
|
|
} else if app.singbox_running {
|
|
nid.hIcon = app.icon_running;
|
|
set_tray_tip(*nid, "Sing-box: Running");
|
|
} else {
|
|
nid.hIcon = app.icon_stopped;
|
|
set_tray_tip(*nid, "Sing-box: Stopped");
|
|
}
|
|
|
|
Shell_NotifyIconW(NIM_MODIFY, *nid);
|
|
}
|
|
|
|
clear_last_singbox_logs :: () {
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
defer LeaveCriticalSection(*global_singbox_log_mutex);
|
|
|
|
for i: 0..global_last_singbox_lines_count-1 {
|
|
free(global_last_singbox_lines[i]);
|
|
}
|
|
global_last_singbox_lines_count = 0;
|
|
}
|
|
|
|
get_last_singbox_logs_string :: () -> string {
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
defer LeaveCriticalSection(*global_singbox_log_mutex);
|
|
|
|
builder: String_Builder;
|
|
for i: 0..global_last_singbox_lines_count-1 {
|
|
append(*builder, global_last_singbox_lines[i]);
|
|
append(*builder, "\n");
|
|
}
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
find_singbox_in_userspace :: () -> string {
|
|
search_path := tprint("%\\*", global_userspace_dir);
|
|
find_data: WIN32_FIND_DATAW;
|
|
hFind := FindFirstFileW(utf8_to_wide(search_path), *find_data);
|
|
if hFind != INVALID_HANDLE_VALUE {
|
|
defer FindClose(hFind);
|
|
while true {
|
|
filename := wide_to_utf8(find_data.cFileName.data);
|
|
defer free(filename);
|
|
|
|
if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
if filename != "." && filename != ".." {
|
|
sub_exe := tprint("%\\%\\sing-box.exe", global_userspace_dir, filename);
|
|
if file_exists(sub_exe) {
|
|
return sub_exe;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !FindNextFileW(hFind, *find_data) break;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
run_command_blocking :: (cmd: string, working_dir: string = "") -> success: bool, exit_code: u32 {
|
|
startup_info: STARTUPINFOW;
|
|
startup_info.cb = size_of(STARTUPINFOW);
|
|
startup_info.dwFlags = STARTF_USESHOWWINDOW;
|
|
startup_info.wShowWindow = SW_HIDE;
|
|
|
|
process_info: PROCESS_INFORMATION;
|
|
CREATE_NO_WINDOW :: 0x08000000;
|
|
|
|
cmd_wide := utf8_to_wide(cmd);
|
|
working_dir_wide: *u16 = null;
|
|
if working_dir.count > 0 {
|
|
working_dir_wide = utf8_to_wide(working_dir);
|
|
}
|
|
|
|
success := CreateProcessW(
|
|
null,
|
|
cmd_wide,
|
|
null,
|
|
null,
|
|
0,
|
|
CREATE_NO_WINDOW,
|
|
null,
|
|
working_dir_wide,
|
|
*startup_info,
|
|
*process_info
|
|
);
|
|
|
|
if !success return false, 0;
|
|
|
|
WaitForSingleObject(process_info.hProcess, INFINITE);
|
|
|
|
exit_code: u32;
|
|
GetExitCodeProcess(process_info.hProcess, *exit_code);
|
|
|
|
CloseHandle(process_info.hProcess);
|
|
CloseHandle(process_info.hThread);
|
|
|
|
return true, exit_code;
|
|
}
|
|
|
|
get_command_output :: (cmd: string, working_dir: string = "") -> string {
|
|
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) return "";
|
|
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
startup_info: STARTUPINFOW;
|
|
startup_info.cb = size_of(STARTUPINFOW);
|
|
startup_info.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
|
|
startup_info.wShowWindow = SW_HIDE;
|
|
startup_info.hStdOutput = hWritePipe;
|
|
startup_info.hStdError = hWritePipe;
|
|
|
|
process_info: PROCESS_INFORMATION;
|
|
CREATE_NO_WINDOW :: 0x08000000;
|
|
|
|
cmd_wide := utf8_to_wide(cmd);
|
|
working_dir_wide: *u16 = null;
|
|
if working_dir.count > 0 {
|
|
working_dir_wide = utf8_to_wide(working_dir);
|
|
}
|
|
|
|
success := CreateProcessW(
|
|
null,
|
|
cmd_wide,
|
|
null,
|
|
null,
|
|
1, // TRUE to inherit handles
|
|
CREATE_NO_WINDOW,
|
|
null,
|
|
working_dir_wide,
|
|
*startup_info,
|
|
*process_info
|
|
);
|
|
|
|
CloseHandle(hWritePipe); // Close write end so read returns 0 on EOF
|
|
|
|
if !success {
|
|
CloseHandle(hReadPipe);
|
|
return "";
|
|
}
|
|
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
|
|
buffer: [1024] u8;
|
|
bytes_read: DWORD;
|
|
while true {
|
|
ok := ReadFile(hReadPipe, buffer.data, xx buffer.count, *bytes_read, null);
|
|
if !ok || bytes_read == 0 break;
|
|
append(*builder, buffer.data, xx bytes_read);
|
|
}
|
|
|
|
WaitForSingleObject(process_info.hProcess, INFINITE);
|
|
CloseHandle(process_info.hProcess);
|
|
CloseHandle(process_info.hThread);
|
|
CloseHandle(hReadPipe);
|
|
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
get_local_singbox_version :: () -> string {
|
|
exe_path := tprint("%\\sing-box.exe", global_userspace_dir);
|
|
if !file_exists(exe_path) return copy_string("Not Installed");
|
|
|
|
cmd := tprint("\"%\" version", exe_path);
|
|
output := get_command_output(cmd, global_userspace_dir);
|
|
defer free(output);
|
|
|
|
if output.count == 0 return copy_string("Unknown");
|
|
|
|
lines := split(output, "\n");
|
|
defer array_free(lines);
|
|
|
|
if lines.count > 0 {
|
|
first_line := trim(lines[0]);
|
|
prefix := "sing-box version ";
|
|
if starts_with(first_line, prefix) {
|
|
ver := slice(first_line, prefix.count, first_line.count - prefix.count);
|
|
space_idx := find_index_from_left(ver, " ");
|
|
if space_idx != -1 {
|
|
ver = slice(ver, 0, space_idx);
|
|
}
|
|
return copy_string(ver);
|
|
}
|
|
return copy_string(first_line);
|
|
}
|
|
|
|
return copy_string("Unknown");
|
|
}
|
|
|
|
fetch_latest_web_version :: () -> string {
|
|
url := "https://api.github.com/repos/SagerNet/sing-box/releases/latest";
|
|
|
|
hInternet := InternetOpenW(
|
|
utf8_to_wide("Sing-boxTray"),
|
|
INTERNET_OPEN_TYPE_PRECONFIG,
|
|
null,
|
|
null,
|
|
0
|
|
);
|
|
if !hInternet return "";
|
|
defer InternetCloseHandle(hInternet);
|
|
|
|
headers := "User-Agent: Sing-boxTray\r\n";
|
|
hUrl := InternetOpenUrlW(
|
|
hInternet,
|
|
utf8_to_wide(url),
|
|
utf8_to_wide(headers),
|
|
cast(u32) headers.count,
|
|
INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_SECURE,
|
|
0
|
|
);
|
|
if !hUrl return "";
|
|
defer InternetCloseHandle(hUrl);
|
|
|
|
response_code: DWORD;
|
|
response_code_size: DWORD = size_of(type_of(response_code));
|
|
if HttpQueryInfoW(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, *response_code, *response_code_size, null) {
|
|
if response_code != 200 return "";
|
|
} else {
|
|
return "";
|
|
}
|
|
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
|
|
buffer: [4096] u8;
|
|
bytes_read: DWORD;
|
|
while true {
|
|
ok := InternetReadFile(hUrl, buffer.data, xx buffer.count, *bytes_read);
|
|
if !ok || bytes_read == 0 break;
|
|
append(*builder, buffer.data, xx bytes_read);
|
|
}
|
|
|
|
json_data := builder_to_string(*builder);
|
|
defer free(json_data);
|
|
|
|
tag := get_json_string_field(json_data, "\"tag_name\"");
|
|
if tag.count > 0 {
|
|
if tag[0] == #char "v" {
|
|
return copy_string(slice(tag, 1, tag.count - 1));
|
|
}
|
|
return copy_string(tag);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
Version_Checker_Data :: struct {
|
|
app: *App_State;
|
|
}
|
|
|
|
version_checker_thread_proc :: (thread: *Thread) -> s64 {
|
|
data := cast(*Version_Checker_Data) thread.data;
|
|
if !data return 1;
|
|
|
|
latest := fetch_latest_web_version();
|
|
if latest.count > 0 {
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
if data.app.latest_web_version.count > 0 {
|
|
free(data.app.latest_web_version);
|
|
}
|
|
data.app.latest_web_version = latest;
|
|
LeaveCriticalSection(*global_singbox_log_mutex);
|
|
} else {
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
if data.app.latest_web_version.count > 0 {
|
|
free(data.app.latest_web_version);
|
|
}
|
|
data.app.latest_web_version = copy_string("Unknown");
|
|
LeaveCriticalSection(*global_singbox_log_mutex);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
install_or_update_singbox_core :: (app: *App_State, is_update: bool) -> bool {
|
|
clear_last_singbox_logs();
|
|
|
|
version_to_download := "1.13.14"; // fallback
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
if app.latest_web_version != "Checking..." && app.latest_web_version != "Unknown" && app.latest_web_version.count > 0 {
|
|
version_to_download = app.latest_web_version;
|
|
}
|
|
LeaveCriticalSection(*global_singbox_log_mutex);
|
|
|
|
download_url := tprint("https://github.com/SagerNet/sing-box/releases/download/v%/sing-box-%-windows-amd64.zip", version_to_download, version_to_download);
|
|
temp_zip := tprint("%\\sing-box-temp.zip", global_userspace_dir);
|
|
DeleteFileW(utf8_to_wide(temp_zip)); // remove any stale/partial from previous failed attempt
|
|
exe_path := tprint("%\\sing-box.exe", global_userspace_dir);
|
|
|
|
prompt_title := ifx is_update then "Update Sing-box" else "Sing-box Core Installation";
|
|
success_msg := tprint("Sing-box core % successfully!", ifx is_update then "updated" else "installed");
|
|
|
|
file_size, size_ok, size_err := get_remote_file_size(download_url);
|
|
if !size_ok {
|
|
msg := tprint("Could not determine the Sing-box download size.\nError: %\n\nPlease ensure you have an active internet connection.", size_err);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide(prompt_title), MB_ICONERROR);
|
|
return false;
|
|
}
|
|
|
|
size_text := format_file_size(file_size);
|
|
action := ifx is_update then "update Sing-box" else "download and install Sing-box";
|
|
prompt := tprint("Version: %\nDownload size: %\n\nWould you like to % now?", version_to_download, size_text, action);
|
|
response := MessageBoxW(app.hwnd, utf8_to_wide(prompt), utf8_to_wide(prompt_title), MB_YESNO | MB_ICONQUESTION);
|
|
if response != IDYES {
|
|
return false;
|
|
}
|
|
|
|
// 1. Download while VPN is still active
|
|
success, err_msg := show_download_progress_dialog(app.hwnd, download_url, temp_zip, file_size, is_update);
|
|
if !success {
|
|
msg := tprint("Failed to download Sing-box core.\nError: %\n\nPlease ensure you have an active internet connection.", err_msg);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
if err_msg.count > 0 { free(err_msg); }
|
|
return false;
|
|
}
|
|
if err_msg.count > 0 { free(err_msg); } // free the (copied) error string returned from dialog on success path too (usually "")
|
|
|
|
// 2. Stop and forcefully terminate any running sing-box instances to unlock the files
|
|
was_running := app.singbox_running;
|
|
if was_running {
|
|
stop_singbox(app);
|
|
}
|
|
|
|
// Kill any other running instances of sing-box.exe (e.g. from the main tray app or orphaned processes)
|
|
run_command_blocking("taskkill /f /im sing-box.exe");
|
|
Sleep(500); // Give OS some time to release file locks
|
|
|
|
// 3. Extract the zip file
|
|
tar_cmd := tprint("tar -xf \"%\" -C \"%\"", temp_zip, global_userspace_dir);
|
|
tar_ok, exit_code := run_command_blocking(tar_cmd);
|
|
if !tar_ok || exit_code != 0 {
|
|
// Fallback to powershell
|
|
ps_cmd := tprint("powershell.exe -NoProfile -NonInteractive -Command \"Expand-Archive -Path '%' -DestinationPath '%' -Force\"", temp_zip, global_userspace_dir);
|
|
ps_ok, ps_exit := run_command_blocking(ps_cmd);
|
|
if !ps_ok || ps_exit != 0 {
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Failed to extract Sing-box core zip archive using tar or PowerShell."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
DeleteFileW(utf8_to_wide(temp_zip));
|
|
if was_running start_singbox(app);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
found_exe := find_singbox_in_userspace();
|
|
if found_exe.count > 0 {
|
|
if found_exe != exe_path {
|
|
// Delete old files first to make sure MoveFileW does not fail
|
|
DeleteFileW(utf8_to_wide(exe_path));
|
|
move_ok := MoveFileW(utf8_to_wide(found_exe), utf8_to_wide(exe_path));
|
|
if !move_ok {
|
|
err := GetLastError();
|
|
msg := tprint("Failed to replace sing-box.exe. Error code: %", err);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
DeleteFileW(utf8_to_wide(temp_zip));
|
|
if was_running start_singbox(app);
|
|
return false;
|
|
}
|
|
|
|
// Also move libcronet.dll if present
|
|
dir_part := found_exe;
|
|
if ends_with(dir_part, "\\sing-box.exe") {
|
|
dir_part.count -= 13;
|
|
}
|
|
lib_path := tprint("%\\libcronet.dll", dir_part);
|
|
if file_exists(lib_path) {
|
|
dest_lib := tprint("%\\libcronet.dll", global_userspace_dir);
|
|
DeleteFileW(utf8_to_wide(dest_lib));
|
|
MoveFileW(utf8_to_wide(lib_path), utf8_to_wide(dest_lib));
|
|
}
|
|
|
|
// Clean up the extracted subdirectory
|
|
rm_cmd := tprint("cmd.exe /c rmdir /s /q \"%\"", dir_part);
|
|
run_command_blocking(rm_cmd);
|
|
}
|
|
} else {
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Could not locate sing-box.exe in the extracted files."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
DeleteFileW(utf8_to_wide(temp_zip));
|
|
if was_running start_singbox(app);
|
|
return false;
|
|
}
|
|
|
|
DeleteFileW(utf8_to_wide(temp_zip));
|
|
|
|
// Update cached local version
|
|
new_ver := get_local_singbox_version();
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
if app.local_version.count > 0 {
|
|
free(app.local_version);
|
|
}
|
|
app.local_version = new_ver;
|
|
LeaveCriticalSection(*global_singbox_log_mutex);
|
|
|
|
MessageBoxW(app.hwnd, utf8_to_wide(success_msg), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION);
|
|
|
|
// Always start/restart to ensure the connection is restored immediately
|
|
start_singbox(app);
|
|
|
|
return true;
|
|
}
|
|
|
|
modify_config_cache_file :: (json: string, cache_path: string) -> string {
|
|
replace_target :: "\"cache_file\": {";
|
|
replace_idx := find_index_from_left(json, replace_target);
|
|
if replace_idx != -1 {
|
|
builder: String_Builder;
|
|
append(*builder, slice(json, 0, replace_idx + replace_target.count));
|
|
append(*builder, tprint(" \"path\": \"%\",", cache_path));
|
|
append(*builder, slice(json, replace_idx + replace_target.count, json.count - (replace_idx + replace_target.count)));
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
return copy_string(json);
|
|
}
|
|
|
|
start_singbox :: (app: *App_State) -> bool {
|
|
if app.singbox_running return true;
|
|
|
|
config_filename := global_config_path;
|
|
config_run_filename := global_config_run_path;
|
|
|
|
has_config := file_exists(config_filename);
|
|
url_present := false;
|
|
has_outbounds := false;
|
|
|
|
if has_config {
|
|
config_data, read_ok := read_entire_file(config_filename);
|
|
if read_ok {
|
|
url := get_json_string_field(config_data, "\"_url\"");
|
|
if url {
|
|
url_present = true;
|
|
}
|
|
if find_index_from_left(config_data, "\"outbounds\"") != -1 && find_index_from_left(config_data, "\"route\"") != -1 {
|
|
has_outbounds = true;
|
|
}
|
|
free(config_data);
|
|
}
|
|
}
|
|
|
|
if !has_config || !has_outbounds {
|
|
if !url_present {
|
|
// Prompt configuration dialog
|
|
changed := show_config_url_dialog(app.hwnd, app.is_test_mode);
|
|
if changed {
|
|
// reload config to verify and fetch URL
|
|
config_data, read_ok := read_entire_file(config_filename);
|
|
if read_ok {
|
|
url := get_json_string_field(config_data, "\"_url\"");
|
|
if url {
|
|
url_present = true;
|
|
}
|
|
free(config_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if url_present {
|
|
// Trigger animation for downloading config
|
|
app.is_updating = true;
|
|
app.animation_frame = 0;
|
|
SetTimer(app.hwnd, TIMER_ANIMATION, 150, null);
|
|
update_tray(app);
|
|
|
|
changed, success, err_msg := perform_update(app.is_test_mode);
|
|
|
|
app.is_updating = false;
|
|
KillTimer(app.hwnd, TIMER_ANIMATION);
|
|
update_tray(app);
|
|
|
|
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);
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error
|
|
clean_config := strip_json_metadata(modified);
|
|
|
|
// Redirect cache file in test mode to avoid database locking collisions
|
|
final_config: string;
|
|
if app.is_test_mode {
|
|
final_config = modify_config_cache_file(clean_config, "cache_test.db");
|
|
free(clean_config);
|
|
} else {
|
|
final_config = clean_config;
|
|
}
|
|
|
|
write_entire_file(config_run_filename, final_config);
|
|
log_print("Generated clean % for sing-box (port %).\n", config_run_filename, app.port);
|
|
|
|
free(config_data);
|
|
free(modified);
|
|
free(final_config);
|
|
}
|
|
}
|
|
|
|
exe_path := tprint("%\\sing-box.exe", global_userspace_dir);
|
|
if !file_exists(exe_path) {
|
|
installed := install_or_update_singbox_core(app, false);
|
|
return installed; // install_or_update_singbox_core handles prompting and starting sing-box
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
clear_last_singbox_logs();
|
|
cmd := tprint("% run -c %", exe_path, config_run_filename);
|
|
job, pi, ok := create_process_hidden(cmd, hWritePipe, global_userspace_dir);
|
|
|
|
if hWritePipe != INVALID_HANDLE_VALUE {
|
|
CloseHandle(hWritePipe);
|
|
}
|
|
|
|
if !ok {
|
|
if hReadPipe != INVALID_HANDLE_VALUE CloseHandle(hReadPipe);
|
|
msg := tprint("Failed to start Sing-box process.\n\nError: Could not spawn the process.\n\nPlease check the logs for more details at:\n%", global_log_path);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Error"), MB_ICONERROR);
|
|
return false;
|
|
}
|
|
|
|
app.singbox_process_handle = pi.hProcess;
|
|
app.singbox_thread_handle = pi.hThread;
|
|
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 && !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);
|
|
log_print("Sing-box started successfully.\n");
|
|
return true;
|
|
}
|
|
|
|
stop_singbox :: (app: *App_State) {
|
|
app.is_connecting = false;
|
|
KillTimer(app.hwnd, TIMER_ANIMATION);
|
|
|
|
if !app.singbox_running return;
|
|
|
|
TerminateProcess(app.singbox_process_handle, 0);
|
|
WaitForSingleObject(app.singbox_process_handle, 3000);
|
|
CloseHandle(app.singbox_process_handle);
|
|
CloseHandle(app.singbox_thread_handle);
|
|
CloseHandle(app.singbox_job_handle);
|
|
|
|
app.singbox_process_handle = null;
|
|
app.singbox_thread_handle = null;
|
|
app.singbox_job_handle = null;
|
|
app.singbox_running = false;
|
|
|
|
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");
|
|
}
|
|
|
|
DeleteFileW(utf8_to_wide(global_config_run_path));
|
|
|
|
update_tray(app);
|
|
log_print("Sing-box stopped.\n");
|
|
}
|
|
|
|
check_process_status :: (app: *App_State) {
|
|
if !app.singbox_running return;
|
|
|
|
exit_code: u32;
|
|
success := GetExitCodeProcess(app.singbox_process_handle, *exit_code);
|
|
STILL_ACTIVE :: 259;
|
|
if success && exit_code != STILL_ACTIVE {
|
|
log_error("Sing-box process exited unexpectedly with code %.\n", exit_code);
|
|
|
|
CloseHandle(app.singbox_process_handle);
|
|
CloseHandle(app.singbox_thread_handle);
|
|
CloseHandle(app.singbox_job_handle);
|
|
|
|
app.singbox_process_handle = null;
|
|
app.singbox_thread_handle = null;
|
|
app.singbox_job_handle = null;
|
|
app.singbox_running = false;
|
|
|
|
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_warn("System proxy disabled due to unexpected exit.\n");
|
|
} else {
|
|
log_info("Test Mode: System proxy bypass kept on unexpected exit.\n");
|
|
}
|
|
|
|
DeleteFileW(utf8_to_wide(global_config_run_path));
|
|
|
|
update_tray(app);
|
|
|
|
logs_str := get_last_singbox_logs_string();
|
|
defer free(logs_str);
|
|
|
|
msg := tprint("Sing-box process exited unexpectedly with code %.\n\nLast output logs:\n%\nFor more details, please view the full log file at:\n%", exit_code, logs_str, global_log_path);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Process Error"), MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
trigger_immediate_update :: (app: *App_State) {
|
|
app.is_updating = true;
|
|
app.animation_frame = 0;
|
|
SetTimer(app.hwnd, TIMER_ANIMATION, 150, null);
|
|
update_tray(app);
|
|
|
|
changed, success, err_msg := perform_update(app.is_test_mode);
|
|
|
|
app.is_updating = false;
|
|
|
|
if success {
|
|
if changed {
|
|
log_print("Config updated, restarting Sing-box...\n");
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
start_singbox(app);
|
|
} else {
|
|
start_singbox(app);
|
|
}
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Configuration updated and Sing-box restarted successfully."), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION);
|
|
} else {
|
|
if !app.singbox_running {
|
|
start_singbox(app);
|
|
} else {
|
|
KillTimer(app.hwnd, TIMER_ANIMATION);
|
|
update_tray(app);
|
|
}
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Configuration is already up-to-date."), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION);
|
|
}
|
|
} else {
|
|
KillTimer(app.hwnd, TIMER_ANIMATION);
|
|
update_tray(app);
|
|
msg := tprint("Failed to download configuration.\nError: %", err_msg);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
is_newer_version :: (current: string, latest: string) -> bool {
|
|
if current.count == 0 || latest.count == 0 return false;
|
|
if current == "Unknown" || latest == "Unknown" || latest == "Checking..." return false;
|
|
if current == "Not Installed" return false;
|
|
|
|
c_parts := split(current, ".");
|
|
defer array_free(c_parts);
|
|
l_parts := split(latest, ".");
|
|
defer array_free(l_parts);
|
|
|
|
min_parts := ifx c_parts.count < l_parts.count then c_parts.count else l_parts.count;
|
|
|
|
for i: 0..min_parts-1 {
|
|
c_val, c_ok := to_integer(trim(c_parts[i]));
|
|
l_val, l_ok := to_integer(trim(l_parts[i]));
|
|
if c_ok && l_ok {
|
|
if l_val > c_val return true;
|
|
if c_val > l_val return false;
|
|
}
|
|
}
|
|
|
|
if l_parts.count > c_parts.count return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
show_context_menu :: (app: *App_State) {
|
|
hMenu := CreatePopupMenu();
|
|
if !hMenu return;
|
|
defer DestroyMenu(hMenu);
|
|
|
|
AppendMenuW(hMenu, MF_OWNERDRAW, CMD_STATUS, null);
|
|
|
|
AppendMenuW(hMenu, MF_SEPARATOR, 0, null);
|
|
|
|
AppendMenuW(hMenu, MF_STRING, CMD_RESTART, utf8_to_wide("Restart"));
|
|
toggle_str := ifx app.singbox_running then "Stop Sing-box" else "Start Sing-box";
|
|
AppendMenuW(hMenu, MF_STRING, CMD_START_STOP, utf8_to_wide(toggle_str));
|
|
AppendMenuW(hMenu, MF_STRING, CMD_UPDATE_NOW, utf8_to_wide("Update Config Now"));
|
|
|
|
hConfigureMenu := CreatePopupMenu();
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_SET_URL, utf8_to_wide("Configure URL..."));
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_SET_PORT, utf8_to_wide("Configure Port..."));
|
|
autostart_flags := MF_STRING;
|
|
if is_autostart_enabled() {
|
|
autostart_flags |= MF_CHECKED;
|
|
}
|
|
AppendMenuW(hConfigureMenu, autostart_flags, CMD_TOGGLE_AUTOSTART, utf8_to_wide("Start on Windows boot"));
|
|
|
|
AppendMenuW(hConfigureMenu, MF_SEPARATOR, 0, null);
|
|
|
|
EnterCriticalSection(*global_singbox_log_mutex);
|
|
local_ver := copy_string(app.local_version);
|
|
latest_ver := copy_string(app.latest_web_version);
|
|
LeaveCriticalSection(*global_singbox_log_mutex);
|
|
defer free(local_ver);
|
|
defer free(latest_ver);
|
|
|
|
local_ver_str := tprint("Current Core Version: %", local_ver);
|
|
AppendMenuW(hConfigureMenu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, utf8_to_wide(local_ver_str));
|
|
|
|
is_newer := is_newer_version(local_ver, latest_ver);
|
|
if is_newer {
|
|
latest_ver_str := tprint("✨ Update Available: %! 🚀", latest_ver);
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide(latest_ver_str));
|
|
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide("👉 Update Sing-box Core Now!"));
|
|
} else {
|
|
latest_ver_str := tprint("Latest Core Version: %", latest_ver);
|
|
AppendMenuW(hConfigureMenu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, utf8_to_wide(latest_ver_str));
|
|
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide("Update Sing-box Core"));
|
|
}
|
|
|
|
AppendMenuW(hConfigureMenu, MF_SEPARATOR, 0, null);
|
|
|
|
hUpdateMenu := CreatePopupMenu();
|
|
current_interval := app.updater_data.update_interval_seconds;
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 0 then MF_CHECKED else 0), CMD_UPDATE_NEVER, utf8_to_wide("Never"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 1800 then MF_CHECKED else 0), CMD_UPDATE_30M, utf8_to_wide("30 minutes"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 3600 then MF_CHECKED else 0), CMD_UPDATE_1H, utf8_to_wide("1 hour"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 21600 then MF_CHECKED else 0), CMD_UPDATE_6H, utf8_to_wide("6 hours"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 43200 then MF_CHECKED else 0), CMD_UPDATE_12H, utf8_to_wide("12 hours"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 86400 then MF_CHECKED else 0), CMD_UPDATE_DAILY, utf8_to_wide("Daily"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 259200 then MF_CHECKED else 0), CMD_UPDATE_3D, utf8_to_wide("3 days"));
|
|
AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 604800 then MF_CHECKED else 0), CMD_UPDATE_WEEKLY, utf8_to_wide("weekly"));
|
|
AppendMenuW(hConfigureMenu, MF_POPUP, cast(s64) hUpdateMenu, utf8_to_wide("Auto-update"));
|
|
AppendMenuW(hConfigureMenu, MF_SEPARATOR, 0, null);
|
|
AppendMenuW(hConfigureMenu, MF_STRING, CMD_CONFIG_DIR, utf8_to_wide("Config directory..."));
|
|
|
|
AppendMenuW(hMenu, MF_POPUP, cast(s64) hConfigureMenu, utf8_to_wide("Configure"));
|
|
|
|
AppendMenuW(hMenu, MF_SEPARATOR, 0, null);
|
|
|
|
proxy_flags := MF_STRING;
|
|
sys_proxy_flags := MF_STRING;
|
|
if app.use_system_proxy {
|
|
sys_proxy_flags |= MF_CHECKED;
|
|
} else {
|
|
proxy_flags |= MF_CHECKED;
|
|
}
|
|
AppendMenuW(hMenu, proxy_flags, CMD_MODE_PROXY, utf8_to_wide("Proxy Mode (Local only)"));
|
|
AppendMenuW(hMenu, sys_proxy_flags, CMD_MODE_SYS_PROXY, utf8_to_wide("System Proxy Mode"));
|
|
|
|
AppendMenuW(hMenu, MF_SEPARATOR, 0, null);
|
|
|
|
AppendMenuW(hMenu, MF_STRING, CMD_EXIT, utf8_to_wide("Exit"));
|
|
|
|
cursor_pos: POINT;
|
|
GetCursorPos(*cursor_pos);
|
|
|
|
SetForegroundWindow(app.hwnd);
|
|
|
|
track_flags := TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD;
|
|
cmd := TrackPopupMenu(hMenu, track_flags, cursor_pos.x, cursor_pos.y, 0, app.hwnd, null);
|
|
|
|
if cmd == {
|
|
case CMD_RESTART; {
|
|
log_print("Restart requested, forcing config refresh to select new server.\n");
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
}
|
|
|
|
// Force re-download of config (remote may assign different server) then (re)start sing-box.
|
|
app.is_updating = true;
|
|
app.animation_frame = 0;
|
|
SetTimer(app.hwnd, TIMER_ANIMATION, 150, null);
|
|
update_tray(app);
|
|
|
|
_, success, err_msg := perform_update(app.is_test_mode);
|
|
|
|
app.is_updating = false;
|
|
KillTimer(app.hwnd, TIMER_ANIMATION);
|
|
update_tray(app);
|
|
|
|
if success {
|
|
start_singbox(app);
|
|
} else {
|
|
msg := tprint("Failed to refresh configuration for restart.\nError: %", err_msg);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
}
|
|
}
|
|
case CMD_START_STOP; {
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
} else {
|
|
start_singbox(app);
|
|
}
|
|
}
|
|
case CMD_SET_URL; {
|
|
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, app.is_test_mode);
|
|
if changed {
|
|
log_print("Port configured, updating state...\n");
|
|
|
|
config_filename := global_config_path;
|
|
config_data, read_ok := read_entire_file(config_filename);
|
|
if read_ok {
|
|
port_str := get_json_val_field(config_data, "\"_port\"");
|
|
if port_str {
|
|
val, ok := to_integer(port_str);
|
|
if ok {
|
|
app.port = xx val;
|
|
log_print("Port updated to %.\n", app.port);
|
|
}
|
|
}
|
|
free(config_data);
|
|
}
|
|
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
start_singbox(app);
|
|
}
|
|
}
|
|
}
|
|
case CMD_UPDATE_NOW; {
|
|
trigger_immediate_update(app);
|
|
}
|
|
case CMD_MODE_PROXY; {
|
|
if app.use_system_proxy {
|
|
app.use_system_proxy = false;
|
|
|
|
config_filename := global_config_path;
|
|
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, app.updater_data.update_interval_seconds);
|
|
write_entire_file(config_filename, updated);
|
|
free(config_data);
|
|
free(updated);
|
|
}
|
|
|
|
log_print("Switched to Proxy Mode.\n");
|
|
if app.singbox_running {
|
|
if !app.is_test_mode {
|
|
set_windows_system_proxy(false, "");
|
|
log_print("System proxy disabled.\n");
|
|
}
|
|
stop_singbox(app);
|
|
start_singbox(app);
|
|
}
|
|
}
|
|
}
|
|
case CMD_MODE_SYS_PROXY; {
|
|
if !app.use_system_proxy {
|
|
app.use_system_proxy = true;
|
|
|
|
config_filename := global_config_path;
|
|
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, app.updater_data.update_interval_seconds);
|
|
write_entire_file(config_filename, updated);
|
|
free(config_data);
|
|
free(updated);
|
|
}
|
|
|
|
log_print("Switched to System Proxy Mode.\n");
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
start_singbox(app);
|
|
}
|
|
}
|
|
}
|
|
case CMD_TOGGLE_AUTOSTART; {
|
|
currently_enabled := is_autostart_enabled();
|
|
success := set_autostart(!currently_enabled);
|
|
if success {
|
|
new_state := !currently_enabled;
|
|
if new_state {
|
|
log_info("Autostart on boot enabled.\n");
|
|
} else {
|
|
log_info("Autostart on boot disabled.\n");
|
|
}
|
|
} else {
|
|
log_error("Failed to toggle autostart registry configuration.\n");
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Failed to update Windows registry autostart configuration."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
}
|
|
}
|
|
case CMD_CONFIG_DIR; {
|
|
ShellExecuteW(null, utf8_to_wide("open"), utf8_to_wide(global_userspace_dir), null, null, 1);
|
|
}
|
|
case CMD_UPDATE_CORE; {
|
|
install_or_update_singbox_core(app, true);
|
|
}
|
|
case CMD_UPDATE_NEVER; {
|
|
save_update_interval(app, 0);
|
|
log_print("Auto-update interval set to: Never\n");
|
|
}
|
|
case CMD_UPDATE_30M; {
|
|
save_update_interval(app, 1800);
|
|
log_print("Auto-update interval set to: 30 minutes\n");
|
|
}
|
|
case CMD_UPDATE_1H; {
|
|
save_update_interval(app, 3600);
|
|
log_print("Auto-update interval set to: 1 hour\n");
|
|
}
|
|
case CMD_UPDATE_6H; {
|
|
save_update_interval(app, 21600);
|
|
log_print("Auto-update interval set to: 6 hours\n");
|
|
}
|
|
case CMD_UPDATE_12H; {
|
|
save_update_interval(app, 43200);
|
|
log_print("Auto-update interval set to: 12 hours\n");
|
|
}
|
|
case CMD_UPDATE_DAILY; {
|
|
save_update_interval(app, 86400);
|
|
log_print("Auto-update interval set to: Daily\n");
|
|
}
|
|
case CMD_UPDATE_3D; {
|
|
save_update_interval(app, 259200);
|
|
log_print("Auto-update interval set to: 3 days\n");
|
|
}
|
|
case CMD_UPDATE_WEEKLY; {
|
|
save_update_interval(app, 604800);
|
|
log_print("Auto-update interval set to: weekly\n");
|
|
}
|
|
case CMD_EXIT; {
|
|
DestroyWindow(app.hwnd);
|
|
}
|
|
}
|
|
|
|
PostMessageW(app.hwnd, WM_NULL, 0, 0);
|
|
}
|
|
|
|
app_wnd_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT #c_call {
|
|
ctx: #Context;
|
|
push_context ctx {
|
|
app := cast(*App_State) GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
|
|
|
if msg == {
|
|
case WM_CREATE; {
|
|
create_struct := cast(*CREATESTRUCTW) lparam;
|
|
app = cast(*App_State) create_struct.lpCreateParams;
|
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, cast(LONG_PTR) app);
|
|
return 0;
|
|
}
|
|
case WM_MEASUREITEM; {
|
|
lpmis := cast(*MEASUREITEMSTRUCT) lparam;
|
|
if lpmis.CtlType == ODT_MENU {
|
|
lpmis.itemWidth = 160;
|
|
lpmis.itemHeight = 22;
|
|
return TRUE;
|
|
}
|
|
}
|
|
case WM_DRAWITEM; {
|
|
lpdis := cast(*DRAWITEMSTRUCT) lparam;
|
|
if lpdis.CtlType == ODT_MENU {
|
|
if app {
|
|
hDC := lpdis.hDC;
|
|
rect := lpdis.rcItem;
|
|
|
|
// Draw menu background
|
|
hMenuBrush := GetSysColorBrush(COLOR_MENU);
|
|
FillRect(hDC, *rect, hMenuBrush);
|
|
|
|
// Create a bold Segoe UI font for high readability
|
|
face_name := utf8_to_wide("Segoe UI");
|
|
hBoldFont := CreateFontW(
|
|
-13, 0, 0, 0, FW_BOLD, 0, 0, 0,
|
|
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, face_name
|
|
);
|
|
hOldFont := SelectObject(hDC, hBoldFont);
|
|
defer {
|
|
SelectObject(hDC, hOldFont);
|
|
DeleteObject(hBoldFont);
|
|
}
|
|
|
|
// Status and status dot color selection
|
|
status_text := "";
|
|
dot_color: u32 = 0;
|
|
|
|
if app.is_connecting {
|
|
status_text = "Sing-box: Connecting...";
|
|
dot_color = RGB(241, 196, 15); // Yellow/Orange
|
|
} else if app.is_updating {
|
|
status_text = "Sing-box: Updating...";
|
|
dot_color = RGB(241, 196, 15); // Yellow/Orange
|
|
} else if app.singbox_running {
|
|
status_text = "Sing-box: Running";
|
|
dot_color = RGB(39, 174, 96); // Green
|
|
} else {
|
|
status_text = "Sing-box: Stopped";
|
|
dot_color = RGB(219, 68, 85); // Red
|
|
}
|
|
|
|
// Draw status dot (filled circle) in GDI in the 16x16 area
|
|
// Centered 10x10 dot at x: rect.left+9, y: rect.top+6
|
|
hBrush := CreateSolidBrush(dot_color);
|
|
hOldBrush := SelectObject(hDC, hBrush);
|
|
|
|
hPen := CreatePen(PS_NULL, 0, 0);
|
|
hOldPen := SelectObject(hDC, hPen);
|
|
|
|
dot_left := rect.left + 9;
|
|
dot_top := rect.top + 6;
|
|
Ellipse(hDC, dot_left, dot_top, dot_left + 10, dot_top + 10);
|
|
|
|
SelectObject(hDC, hOldPen);
|
|
DeleteObject(hPen);
|
|
SelectObject(hDC, hOldBrush);
|
|
DeleteObject(hBrush);
|
|
|
|
// Draw status text next to the dot (using standard menu text color, which is highly readable)
|
|
text_color := GetSysColor(COLOR_MENUTEXT);
|
|
SetTextColor(hDC, text_color);
|
|
SetBkMode(hDC, TRANSPARENT);
|
|
|
|
text_rect: RECT;
|
|
text_rect.left = rect.left + 28;
|
|
text_rect.top = rect.top + 3;
|
|
text_rect.right = rect.right;
|
|
text_rect.bottom = rect.bottom;
|
|
|
|
DrawTextW(hDC, utf8_to_wide(status_text), -1, *text_rect, DT_SINGLELINE | DT_VCENTER);
|
|
}
|
|
return TRUE;
|
|
}
|
|
}
|
|
case WM_TRAY_CALLBACK; {
|
|
if lparam == WM_RBUTTONUP || lparam == WM_LBUTTONUP {
|
|
show_context_menu(app);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_RESTART_SINGBOX; {
|
|
log_print("WM_RESTART_SINGBOX received, restarting sing-box...\n");
|
|
if app.singbox_running {
|
|
stop_singbox(app);
|
|
start_singbox(app);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_TIMER; {
|
|
if wparam == TIMER_PROCESS_CHECK {
|
|
check_process_status(app);
|
|
} else if wparam == TIMER_ANIMATION {
|
|
if app.is_connecting {
|
|
app.connecting_ticks += 1;
|
|
app.animation_frame = (app.animation_frame + 1) % 4;
|
|
update_tray(app);
|
|
|
|
// Stop connecting animation after 12 ticks (3 seconds at 250ms)
|
|
if app.connecting_ticks >= 12 {
|
|
app.is_connecting = false;
|
|
KillTimer(hwnd, TIMER_ANIMATION);
|
|
update_tray(app);
|
|
}
|
|
} else if app.is_updating {
|
|
app.animation_frame = (app.animation_frame + 1) % 4;
|
|
update_tray(app);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_DESTROY; {
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
}
|
|
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
|
}
|
|
}
|
|
|
|
load_stock_icon :: (siid: s32) -> HICON {
|
|
sii: SHSTOCKICONINFO;
|
|
sii.cbSize = size_of(SHSTOCKICONINFO);
|
|
hr := SHGetStockIconInfo(siid, SHGSI_ICON | SHGSI_SMALLICON, *sii);
|
|
if hr == 0 { // S_OK
|
|
return sii.hIcon;
|
|
}
|
|
// Fallback to default application icon
|
|
return LoadIconW(null, IDI_APPLICATION);
|
|
}
|
|
|
|
global_is_test_mode: bool = false;
|
|
|
|
save_update_interval :: (app: *App_State, interval: s32) {
|
|
app.updater_data.update_interval_seconds = interval;
|
|
|
|
config_filename := global_config_path;
|
|
config_data, read_ok := read_entire_file(config_filename);
|
|
if read_ok {
|
|
url := get_json_string_field(config_data, "\"_url\"");
|
|
mode := get_json_string_field(config_data, "\"_mode\"");
|
|
|
|
mode_val := ifx mode then mode else "system_proxy";
|
|
|
|
updated := set_json_metadata(config_data, url, mode_val, app.port, interval);
|
|
write_entire_file(config_filename, updated);
|
|
|
|
free(config_data);
|
|
free(updated);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
InitializeCriticalSection(*global_singbox_log_mutex);
|
|
|
|
// Change working directory to the executable's directory to resolve relative paths
|
|
// correctly when started via Windows autostart/registry.
|
|
{
|
|
buffer: [2048] u16;
|
|
len := GetModuleFileNameW(null, buffer.data, 2048);
|
|
if len > 0 {
|
|
last_backslash_idx := -1;
|
|
for i: 0..len-1 {
|
|
if buffer[i] == #char "\\" || buffer[i] == #char "/" {
|
|
last_backslash_idx = i;
|
|
}
|
|
}
|
|
if last_backslash_idx != -1 {
|
|
buffer[last_backslash_idx] = 0;
|
|
SetCurrentDirectoryW(buffer.data);
|
|
}
|
|
}
|
|
}
|
|
|
|
init_userspace_paths(is_test);
|
|
|
|
if is_test {
|
|
config_test := global_config_path;
|
|
config_prod := sprint("%\\config.json", global_userspace_dir);
|
|
defer free(config_prod);
|
|
if !file_exists(config_test) && file_exists(config_prod) {
|
|
CopyFileW(utf8_to_wide(config_prod), utf8_to_wide(config_test), FALSE);
|
|
}
|
|
}
|
|
|
|
hInstance := GetModuleHandleW(null);
|
|
class_name_str := ifx is_test then "Sing-boxTrayControllerClassTest" else "Sing-boxTrayControllerClass";
|
|
class_name := utf8_to_wide(class_name_str);
|
|
|
|
wclass: WNDCLASSEXW;
|
|
wclass.cbSize = size_of(WNDCLASSEXW);
|
|
wclass.lpfnWndProc = app_wnd_proc;
|
|
wclass.hInstance = hInstance;
|
|
wclass.hCursor = LoadCursorW(null, IDC_ARROW);
|
|
wclass.lpszClassName = class_name;
|
|
|
|
RegisterClassExW(*wclass);
|
|
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);
|
|
app_state.icon_frames[1] = load_stock_icon(SIID_DRIVENET);
|
|
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 "Sing-box Tray Controller (Test Mode)" else "Sing-box Tray Controller";
|
|
hwnd := CreateWindowExW(
|
|
0,
|
|
class_name,
|
|
utf8_to_wide(title_str),
|
|
0,
|
|
0, 0, 0, 0,
|
|
null,
|
|
null,
|
|
hInstance,
|
|
*app_state
|
|
);
|
|
|
|
if !hwnd {
|
|
log_error("Failed to create hidden window!\n");
|
|
return;
|
|
}
|
|
|
|
app_state.hwnd = hwnd;
|
|
|
|
config_filename := global_config_path;
|
|
|
|
app_state.updater_data.update_interval_seconds = 3600; // default
|
|
|
|
// Load persisted mode, port, and update interval 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;
|
|
}
|
|
|
|
interval_str := get_json_val_field(config_data, "\"_update_interval\"");
|
|
if interval_str {
|
|
val, ok := to_integer(interval_str);
|
|
if ok {
|
|
app_state.updater_data.update_interval_seconds = xx val;
|
|
log_print("Loaded update interval: % seconds\n", app_state.updater_data.update_interval_seconds);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
app_state.local_version = get_local_singbox_version();
|
|
app_state.latest_web_version = copy_string("Checking...");
|
|
|
|
app_state.version_checker_data.app = *app_state;
|
|
thread_init(*app_state.version_checker_thread, version_checker_thread_proc);
|
|
app_state.version_checker_thread.data = *app_state.version_checker_data;
|
|
thread_start(*app_state.version_checker_thread);
|
|
|
|
|
|
nid: NOTIFYICONDATAW;
|
|
nid.cbSize = size_of(NOTIFYICONDATAW);
|
|
nid.hWnd = hwnd;
|
|
nid.uID = 1;
|
|
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
|
|
nid.uCallbackMessage = WM_TRAY_CALLBACK;
|
|
nid.hIcon = app_state.icon_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_error("Failed to register tray icon!\n");
|
|
return;
|
|
}
|
|
defer Shell_NotifyIconW(NIM_DELETE, *nid);
|
|
|
|
// Try to run / start sing-box. On first start (or if config/outbounds are missing),
|
|
// this will prompt the configuration dialog instead of silently failing or erroring.
|
|
start_singbox(*app_state);
|
|
|
|
SetTimer(hwnd, TIMER_PROCESS_CHECK, 1000, null);
|
|
defer KillTimer(hwnd, TIMER_PROCESS_CHECK);
|
|
|
|
app_state.updater_data.hwnd = hwnd;
|
|
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;
|
|
thread_start(*app_state.updater_thread);
|
|
|
|
msg: MSG;
|
|
while GetMessageW(*msg, null, 0, 0) {
|
|
TranslateMessage(*msg);
|
|
DispatchMessageW(*msg);
|
|
}
|
|
|
|
log_print("Exiting application...\n");
|
|
|
|
if app_state.updater_data.stop_event {
|
|
SetEvent(app_state.updater_data.stop_event);
|
|
thread_is_done(*app_state.updater_thread, -1);
|
|
thread_deinit(*app_state.updater_thread);
|
|
CloseHandle(app_state.updater_data.stop_event);
|
|
}
|
|
|
|
if app_state.version_checker_thread.data {
|
|
thread_is_done(*app_state.version_checker_thread, -1);
|
|
thread_deinit(*app_state.version_checker_thread);
|
|
}
|
|
|
|
if app_state.local_version.count > 0 {
|
|
free(app_state.local_version);
|
|
}
|
|
if app_state.latest_web_version.count > 0 {
|
|
free(app_state.latest_web_version);
|
|
}
|
|
|
|
stop_singbox(*app_state);
|
|
|
|
if app_state.icon_running DestroyIcon(app_state.icon_running);
|
|
if app_state.icon_stopped DestroyIcon(app_state.icon_stopped);
|
|
for app_state.icon_frames {
|
|
if it DestroyIcon(it);
|
|
}
|
|
|
|
clear_last_singbox_logs();
|
|
DeleteCriticalSection(*global_singbox_log_mutex);
|
|
}
|