feat: add sing-box updater
This commit is contained in:
parent
b011fcb346
commit
7e26b24030
19
dialog.jai
19
dialog.jai
@ -533,7 +533,16 @@ download_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -
|
|||||||
if state.thread_data.done {
|
if state.thread_data.done {
|
||||||
KillTimer(hwnd, 1);
|
KillTimer(hwnd, 1);
|
||||||
thread_is_done(*state.thread, -1);
|
thread_is_done(*state.thread, -1);
|
||||||
|
// Capture error_msg (which may be temp-allocated inside download thread) BEFORE deinit frees the thread temp storage.
|
||||||
|
captured_success := state.thread_data.success;
|
||||||
|
captured_err := copy_string(state.thread_data.error_msg);
|
||||||
thread_deinit(*state.thread);
|
thread_deinit(*state.thread);
|
||||||
|
// Overwrite with safe copies (heap allocated) so that the post-loop return sees valid data.
|
||||||
|
state.thread_data.success = captured_success;
|
||||||
|
state.thread_data.error_msg = captured_err;
|
||||||
|
// Free the (copied) input paths now that thread is done with them.
|
||||||
|
if state.thread_data.url.count > 0 { free(state.thread_data.url); state.thread_data.url = ""; }
|
||||||
|
if state.thread_data.dest_path.count > 0 { free(state.thread_data.dest_path); state.thread_data.dest_path = ""; }
|
||||||
DestroyWindow(hwnd);
|
DestroyWindow(hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -581,8 +590,8 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str
|
|||||||
dialog_y := (screen_h - dialog_h) / 2;
|
dialog_y := (screen_h - dialog_h) / 2;
|
||||||
|
|
||||||
state: Download_Dialog_State;
|
state: Download_Dialog_State;
|
||||||
state.thread_data.url = url;
|
state.thread_data.url = copy_string(url);
|
||||||
state.thread_data.dest_path = dest_path;
|
state.thread_data.dest_path = copy_string(dest_path);
|
||||||
|
|
||||||
dialog_hwnd := CreateWindowExW(
|
dialog_hwnd := CreateWindowExW(
|
||||||
WS_EX_DLGMODALFRAME,
|
WS_EX_DLGMODALFRAME,
|
||||||
@ -596,7 +605,11 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str
|
|||||||
*state
|
*state
|
||||||
);
|
);
|
||||||
|
|
||||||
if !dialog_hwnd return false, "Failed to create dialog window";
|
if !dialog_hwnd {
|
||||||
|
free(state.thread_data.url);
|
||||||
|
free(state.thread_data.dest_path);
|
||||||
|
return false, "Failed to create dialog window";
|
||||||
|
}
|
||||||
|
|
||||||
if parent_hwnd {
|
if parent_hwnd {
|
||||||
EnableWindow(parent_hwnd, FALSE);
|
EnableWindow(parent_hwnd, FALSE);
|
||||||
|
|||||||
471
main.jai
471
main.jai
@ -111,6 +111,11 @@ App_State :: struct {
|
|||||||
log_reader_thread: Thread;
|
log_reader_thread: Thread;
|
||||||
log_reader_data: Log_Reader_Data;
|
log_reader_data: Log_Reader_Data;
|
||||||
log_reader_started: bool;
|
log_reader_started: bool;
|
||||||
|
|
||||||
|
local_version: string;
|
||||||
|
latest_web_version: string;
|
||||||
|
version_checker_thread: Thread;
|
||||||
|
version_checker_data: Version_Checker_Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log_Level :: enum {
|
Log_Level :: enum {
|
||||||
@ -457,11 +462,6 @@ get_last_singbox_logs_string :: () -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
find_singbox_in_userspace :: () -> string {
|
find_singbox_in_userspace :: () -> string {
|
||||||
target_direct := tprint("%\\sing-box.exe", global_userspace_dir);
|
|
||||||
if file_exists(target_direct) {
|
|
||||||
return target_direct;
|
|
||||||
}
|
|
||||||
|
|
||||||
search_path := tprint("%\\*", global_userspace_dir);
|
search_path := tprint("%\\*", global_userspace_dir);
|
||||||
find_data: WIN32_FIND_DATAW;
|
find_data: WIN32_FIND_DATAW;
|
||||||
hFind := FindFirstFileW(utf8_to_wide(search_path), *find_data);
|
hFind := FindFirstFileW(utf8_to_wide(search_path), *find_data);
|
||||||
@ -528,6 +528,311 @@ run_command_blocking :: (cmd: string, working_dir: string = "") -> success: bool
|
|||||||
return true, exit_code;
|
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 {
|
start_singbox :: (app: *App_State) -> bool {
|
||||||
if app.singbox_running return true;
|
if app.singbox_running return true;
|
||||||
|
|
||||||
@ -601,12 +906,21 @@ start_singbox :: (app: *App_State) -> bool {
|
|||||||
// Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error
|
// Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error
|
||||||
clean_config := strip_json_metadata(modified);
|
clean_config := strip_json_metadata(modified);
|
||||||
|
|
||||||
write_entire_file(config_run_filename, clean_config);
|
// 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);
|
log_print("Generated clean % for sing-box (port %).\n", config_run_filename, app.port);
|
||||||
|
|
||||||
free(config_data);
|
free(config_data);
|
||||||
free(modified);
|
free(modified);
|
||||||
free(clean_config);
|
free(final_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,60 +928,8 @@ start_singbox :: (app: *App_State) -> bool {
|
|||||||
if !file_exists(exe_path) {
|
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);
|
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 {
|
if response == IDYES {
|
||||||
clear_last_singbox_logs();
|
installed := install_or_update_singbox_core(app, false);
|
||||||
|
return installed; // install_or_update_singbox_core handles starting sing-box
|
||||||
download_url := "https://github.com/SagerNet/sing-box/releases/download/v1.13.14/sing-box-1.13.14-windows-amd64.zip";
|
|
||||||
temp_zip := tprint("%\\sing-box-temp.zip", global_userspace_dir);
|
|
||||||
|
|
||||||
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);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
found_exe := find_singbox_in_userspace();
|
|
||||||
if found_exe.count > 0 {
|
|
||||||
if found_exe != exe_path {
|
|
||||||
MoveFileW(utf8_to_wide(found_exe), utf8_to_wide(exe_path));
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
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));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteFileW(utf8_to_wide(temp_zip));
|
|
||||||
MessageBoxW(app.hwnd, utf8_to_wide("Sing-box core installed successfully!"), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION);
|
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -742,6 +1004,7 @@ stop_singbox :: (app: *App_State) {
|
|||||||
if !app.singbox_running return;
|
if !app.singbox_running return;
|
||||||
|
|
||||||
TerminateProcess(app.singbox_process_handle, 0);
|
TerminateProcess(app.singbox_process_handle, 0);
|
||||||
|
WaitForSingleObject(app.singbox_process_handle, 3000);
|
||||||
CloseHandle(app.singbox_process_handle);
|
CloseHandle(app.singbox_process_handle);
|
||||||
CloseHandle(app.singbox_thread_handle);
|
CloseHandle(app.singbox_thread_handle);
|
||||||
CloseHandle(app.singbox_job_handle);
|
CloseHandle(app.singbox_job_handle);
|
||||||
@ -850,6 +1113,32 @@ trigger_immediate_update :: (app: *App_State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
show_context_menu :: (app: *App_State) {
|
||||||
hMenu := CreatePopupMenu();
|
hMenu := CreatePopupMenu();
|
||||||
if !hMenu return;
|
if !hMenu return;
|
||||||
@ -872,6 +1161,33 @@ show_context_menu :: (app: *App_State) {
|
|||||||
}
|
}
|
||||||
AppendMenuW(hConfigureMenu, autostart_flags, CMD_TOGGLE_AUTOSTART, utf8_to_wide("Start on Windows boot"));
|
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();
|
hUpdateMenu := CreatePopupMenu();
|
||||||
current_interval := app.updater_data.update_interval_seconds;
|
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 == 0 then MF_CHECKED else 0), CMD_UPDATE_NEVER, utf8_to_wide("Never"));
|
||||||
@ -1019,6 +1335,9 @@ show_context_menu :: (app: *App_State) {
|
|||||||
case CMD_CONFIG_DIR; {
|
case CMD_CONFIG_DIR; {
|
||||||
ShellExecuteW(null, utf8_to_wide("open"), utf8_to_wide(global_userspace_dir), null, null, 1);
|
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; {
|
case CMD_UPDATE_NEVER; {
|
||||||
save_update_interval(app, 0);
|
save_update_interval(app, 0);
|
||||||
log_print("Auto-update interval set to: Never\n");
|
log_print("Auto-update interval set to: Never\n");
|
||||||
@ -1265,6 +1584,15 @@ main :: () {
|
|||||||
|
|
||||||
init_userspace_paths(is_test);
|
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);
|
hInstance := GetModuleHandleW(null);
|
||||||
class_name_str := ifx is_test then "SingboxTrayControllerClassTest" else "SingboxTrayControllerClass";
|
class_name_str := ifx is_test then "SingboxTrayControllerClassTest" else "SingboxTrayControllerClass";
|
||||||
class_name := utf8_to_wide(class_name_str);
|
class_name := utf8_to_wide(class_name_str);
|
||||||
@ -1351,6 +1679,15 @@ main :: () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: NOTIFYICONDATAW;
|
||||||
nid.cbSize = size_of(NOTIFYICONDATAW);
|
nid.cbSize = size_of(NOTIFYICONDATAW);
|
||||||
nid.hWnd = hwnd;
|
nid.hWnd = hwnd;
|
||||||
@ -1398,6 +1735,18 @@ main :: () {
|
|||||||
CloseHandle(app_state.updater_data.stop_event);
|
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);
|
stop_singbox(*app_state);
|
||||||
|
|
||||||
if app_state.icon_running DestroyIcon(app_state.icon_running);
|
if app_state.icon_running DestroyIcon(app_state.icon_running);
|
||||||
|
|||||||
16
updater.jai
16
updater.jai
@ -15,6 +15,12 @@ Updater_Thread_Data :: struct {
|
|||||||
download_url :: (url: string) -> string, bool, string {
|
download_url :: (url: string) -> string, bool, string {
|
||||||
log_info("Downloading config from URL: %\n", url);
|
log_info("Downloading config from URL: %\n", url);
|
||||||
|
|
||||||
|
if file_exists(url) {
|
||||||
|
content, ok := read_entire_file(url);
|
||||||
|
if ok return content, true, "";
|
||||||
|
return "", false, tprint("Failed to read local file: %", url);
|
||||||
|
}
|
||||||
|
|
||||||
hInternet := InternetOpenW(
|
hInternet := InternetOpenW(
|
||||||
utf8_to_wide("SingboxTrayUpdater"),
|
utf8_to_wide("SingboxTrayUpdater"),
|
||||||
INTERNET_OPEN_TYPE_PRECONFIG,
|
INTERNET_OPEN_TYPE_PRECONFIG,
|
||||||
@ -32,11 +38,12 @@ download_url :: (url: string) -> string, bool, string {
|
|||||||
flags |= INTERNET_FLAG_SECURE;
|
flags |= INTERNET_FLAG_SECURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers := "User-Agent: SingboxTray\r\n";
|
||||||
hUrl := InternetOpenUrlW(
|
hUrl := InternetOpenUrlW(
|
||||||
hInternet,
|
hInternet,
|
||||||
utf8_to_wide(url),
|
utf8_to_wide(url),
|
||||||
null,
|
utf8_to_wide(headers),
|
||||||
0,
|
cast(u32) headers.count,
|
||||||
flags,
|
flags,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@ -361,11 +368,12 @@ download_file :: (url: string, dest_path: string) -> bool, string {
|
|||||||
flags |= INTERNET_FLAG_SECURE;
|
flags |= INTERNET_FLAG_SECURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers := "User-Agent: SingboxTray\r\n";
|
||||||
hUrl := InternetOpenUrlW(
|
hUrl := InternetOpenUrlW(
|
||||||
hInternet,
|
hInternet,
|
||||||
utf8_to_wide(url),
|
utf8_to_wide(url),
|
||||||
null,
|
utf8_to_wide(headers),
|
||||||
0,
|
cast(u32) headers.count,
|
||||||
flags,
|
flags,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|||||||
@ -165,6 +165,8 @@ CMD_UPDATE_DAILY :: 1015;
|
|||||||
CMD_UPDATE_3D :: 1016;
|
CMD_UPDATE_3D :: 1016;
|
||||||
CMD_UPDATE_WEEKLY :: 1017;
|
CMD_UPDATE_WEEKLY :: 1017;
|
||||||
CMD_CONFIG_DIR :: 1018;
|
CMD_CONFIG_DIR :: 1018;
|
||||||
|
CMD_UPDATE_CORE :: 1019;
|
||||||
|
|
||||||
|
|
||||||
// Boolean constants
|
// Boolean constants
|
||||||
FALSE :: 0;
|
FALSE :: 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user