494 lines
15 KiB
Plaintext
494 lines
15 KiB
Plaintext
#import "Basic";
|
|
#import "Windows";
|
|
#import "Windows_Utf8";
|
|
#import "Thread";
|
|
#import "String";
|
|
|
|
#load "win32.jai";
|
|
#load "dialog.jai";
|
|
#load "updater.jai";
|
|
|
|
// Custom constants
|
|
TIMER_PROCESS_CHECK :: 1;
|
|
|
|
IDI_APPLICATION :: cast(*u16) 32512;
|
|
IDI_SHIELD :: cast(*u16) 32518;
|
|
|
|
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;
|
|
}
|
|
|
|
// Custom log print sending logs to OutputDebugStringW
|
|
log_print :: (format_string: string, args: .. Any) {
|
|
formatted := tprint(format_string, .. args);
|
|
OutputDebugStringW(utf8_to_wide(formatted));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Hidden Process Spawning with Job Object configuration and stdout/stderr redirection
|
|
create_process_hidden :: (cmd: string, log_file: string = "") -> HANDLE, PROCESS_INFORMATION, bool {
|
|
startup_info: STARTUPINFOW;
|
|
startup_info.cb = size_of(STARTUPINFOW);
|
|
startup_info.dwFlags = STARTF_USESHOWWINDOW;
|
|
startup_info.wShowWindow = SW_HIDE;
|
|
|
|
file_handle: HANDLE = INVALID_HANDLE_VALUE;
|
|
|
|
if log_file {
|
|
sa: SECURITY_ATTRIBUTES;
|
|
sa.nLength = size_of(SECURITY_ATTRIBUTES);
|
|
sa.bInheritHandle = 1; // TRUE
|
|
sa.lpSecurityDescriptor = null;
|
|
|
|
wide_log := utf8_to_wide(log_file);
|
|
|
|
file_handle = CreateFileW(
|
|
wide_log,
|
|
GENERIC_WRITE,
|
|
FILE_SHARE_READ,
|
|
*sa,
|
|
CREATE_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
null
|
|
);
|
|
|
|
if file_handle != INVALID_HANDLE_VALUE {
|
|
startup_info.dwFlags |= STARTF_USESTDHANDLES;
|
|
startup_info.hStdOutput = file_handle;
|
|
startup_info.hStdError = file_handle;
|
|
}
|
|
}
|
|
|
|
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 file_handle != INVALID_HANDLE_VALUE then cast(BOOL) 1 else cast(BOOL) 0;
|
|
|
|
success := CreateProcessW(
|
|
null,
|
|
cmd_wide,
|
|
null,
|
|
null,
|
|
inherit_handles,
|
|
CREATE_NO_WINDOW,
|
|
null,
|
|
null,
|
|
*startup_info,
|
|
*process_info
|
|
);
|
|
|
|
if success && job_handle {
|
|
AssignProcessToJobObject(job_handle, process_info.hProcess);
|
|
}
|
|
|
|
if file_handle != INVALID_HANDLE_VALUE {
|
|
CloseHandle(file_handle);
|
|
}
|
|
|
|
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.singbox_running {
|
|
nid.hIcon = LoadIconW(null, IDI_SHIELD);
|
|
set_tray_tip(*nid, "Sing-box: Running");
|
|
} else {
|
|
nid.hIcon = LoadIconW(null, IDI_APPLICATION);
|
|
set_tray_tip(*nid, "Sing-box: Stopped");
|
|
}
|
|
|
|
Shell_NotifyIconW(NIM_MODIFY, *nid);
|
|
}
|
|
|
|
start_singbox :: (app: *App_State) -> bool {
|
|
if app.singbox_running return true;
|
|
|
|
if !file_exists("config.json") {
|
|
if file_exists("url.txt") {
|
|
changed, success, err_msg := perform_update();
|
|
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 {
|
|
MessageBoxW(app.hwnd, utf8_to_wide("No configuration found. Please configure the Config URL first."), utf8_to_wide("Sing-box Tray"), MB_ICONWARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if file_exists("config.json") {
|
|
config_data, read_ok := read_entire_file("config.json");
|
|
if read_ok {
|
|
modified := modify_config_inbounds(config_data);
|
|
if modified != config_data {
|
|
write_entire_file("config.json", modified);
|
|
log_print("Existing config.json sanitized to use mixed inbound.\n");
|
|
}
|
|
free(config_data);
|
|
free(modified);
|
|
}
|
|
}
|
|
|
|
exe_path := "sing-box.exe";
|
|
// Check if sing-box.exe is not in current folder, check in the sing-box subfolder
|
|
if !file_exists("sing-box.exe") {
|
|
if file_exists("sing-box/sing-box.exe") {
|
|
exe_path = "sing-box\\sing-box.exe";
|
|
}
|
|
}
|
|
|
|
cmd := tprint("% run -c config.json", exe_path);
|
|
job, pi, ok := create_process_hidden(cmd, "sing-box.log");
|
|
if !ok {
|
|
msg := tprint("Failed to start %. Please ensure sing-box.exe is in the same folder or in the 'sing-box' subfolder.", exe_path);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
return false;
|
|
}
|
|
|
|
app.singbox_process_handle = pi.hProcess;
|
|
app.singbox_thread_handle = pi.hThread;
|
|
app.singbox_job_handle = job;
|
|
app.singbox_running = true;
|
|
|
|
if app.use_system_proxy {
|
|
set_windows_system_proxy(true, "127.0.0.1:20122");
|
|
log_print("System proxy enabled.\n");
|
|
}
|
|
|
|
update_tray(app);
|
|
log_print("Sing-box started successfully.\n");
|
|
return true;
|
|
}
|
|
|
|
stop_singbox :: (app: *App_State) {
|
|
if !app.singbox_running return;
|
|
|
|
TerminateProcess(app.singbox_process_handle, 0);
|
|
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;
|
|
|
|
set_windows_system_proxy(false, "");
|
|
log_print("System proxy disabled.\n");
|
|
|
|
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_print("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;
|
|
|
|
set_windows_system_proxy(false, "");
|
|
log_print("System proxy disabled due to unexpected exit.\n");
|
|
|
|
update_tray(app);
|
|
}
|
|
}
|
|
|
|
trigger_immediate_update :: (app: *App_State) {
|
|
changed, success, err_msg := perform_update();
|
|
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);
|
|
}
|
|
MessageBoxW(app.hwnd, utf8_to_wide("Configuration is already up-to-date."), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION);
|
|
}
|
|
} else {
|
|
msg := tprint("Failed to download configuration.\nError: %", err_msg);
|
|
MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
show_context_menu :: (app: *App_State) {
|
|
hMenu := CreatePopupMenu();
|
|
if !hMenu return;
|
|
defer DestroyMenu(hMenu);
|
|
|
|
status_str := ifx app.singbox_running then "Sing-box: Running" else "Sing-box: Stopped";
|
|
AppendMenuW(hMenu, MF_STRING | MF_GRAYED | MF_DISABLED, CMD_STATUS, utf8_to_wide(status_str));
|
|
|
|
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_SET_URL, utf8_to_wide("Configure URL..."));
|
|
AppendMenuW(hMenu, MF_STRING, CMD_UPDATE_NOW, utf8_to_wide("Update Config Now"));
|
|
|
|
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);
|
|
if changed {
|
|
log_print("URL configured, triggering download...\n");
|
|
trigger_immediate_update(app);
|
|
}
|
|
}
|
|
case CMD_UPDATE_NOW; {
|
|
trigger_immediate_update(app);
|
|
}
|
|
case CMD_MODE_PROXY; {
|
|
if app.use_system_proxy {
|
|
app.use_system_proxy = false;
|
|
write_entire_file("mode.txt", "proxy");
|
|
log_print("Switched to Proxy Mode.\n");
|
|
if app.singbox_running {
|
|
set_windows_system_proxy(false, "");
|
|
log_print("System proxy disabled.\n");
|
|
}
|
|
}
|
|
}
|
|
case CMD_MODE_SYS_PROXY; {
|
|
if !app.use_system_proxy {
|
|
app.use_system_proxy = true;
|
|
write_entire_file("mode.txt", "system_proxy");
|
|
log_print("Switched to System Proxy Mode.\n");
|
|
if app.singbox_running {
|
|
set_windows_system_proxy(true, "127.0.0.1:20122");
|
|
log_print("System proxy enabled.\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_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);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_DESTROY; {
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
}
|
|
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
|
}
|
|
}
|
|
|
|
main :: () {
|
|
hInstance := GetModuleHandleW(null);
|
|
class_name := utf8_to_wide("SingboxTrayControllerClass");
|
|
|
|
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;
|
|
|
|
hwnd := CreateWindowExW(
|
|
0,
|
|
class_name,
|
|
utf8_to_wide("Singbox Tray Controller"),
|
|
0,
|
|
0, 0, 0, 0,
|
|
null,
|
|
null,
|
|
hInstance,
|
|
*app_state
|
|
);
|
|
|
|
if !hwnd {
|
|
log_print("Failed to create hidden window!\n");
|
|
return;
|
|
}
|
|
|
|
app_state.hwnd = hwnd;
|
|
|
|
// Load persisted mode
|
|
mode_data, mode_ok := read_entire_file("mode.txt");
|
|
if mode_ok {
|
|
mode_str := trim(mode_data);
|
|
if mode_str == "proxy" {
|
|
app_state.use_system_proxy = false;
|
|
}
|
|
free(mode_data);
|
|
}
|
|
|
|
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 = LoadIconW(null, IDI_APPLICATION);
|
|
set_tray_tip(*nid, "Sing-box: Stopped");
|
|
|
|
if !Shell_NotifyIconW(NIM_ADD, *nid) {
|
|
log_print("Failed to register tray icon!\n");
|
|
return;
|
|
}
|
|
defer Shell_NotifyIconW(NIM_DELETE, *nid);
|
|
|
|
if file_exists("config.json") {
|
|
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.update_interval_seconds = 3600;
|
|
app_state.updater_data.stop_event = CreateEventW(null, TRUE, FALSE, null);
|
|
|
|
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);
|
|
}
|
|
|
|
stop_singbox(*app_state);
|
|
}
|