sing-box-tray/main.jai

1761 lines
61 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("%\\singbox-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 "singbox_tray_test.log" else "singbox_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("%\\singbox_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 "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 global_log_path.count > 0 then global_log_path else (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);
}
// 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("SingboxTray");
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("SingboxTray");
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("SingboxTray"),
INTERNET_OPEN_TYPE_PRECONFIG,
null,
null,
0
);
if !hInternet return "";
defer InternetCloseHandle(hInternet);
headers := "User-Agent: SingboxTray\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");
// 1. Download while VPN is still active
success, err_msg := show_download_progress_dialog(app.hwnd, download_url, temp_zip);
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) {
response := MessageBoxW(app.hwnd, utf8_to_wide("Sing-box core executable (sing-box.exe) was not found in your userspace folder.\n\nWould you like to download and install it now?"), utf8_to_wide("Sing-box Tray"), MB_YESNO | MB_ICONQUESTION);
if response == IDYES {
installed := install_or_update_singbox_core(app, false);
return installed; // install_or_update_singbox_core handles starting sing-box
} else {
return false;
}
}
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);
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_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 "SingboxTrayControllerClassTest" else "SingboxTrayControllerClass";
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 "Singbox Tray Controller (Test Mode)" else "Singbox 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);
}