#import "Basic"; #import "File"; #import "Thread"; #import "Windows"; #import "String"; Updater_Thread_Data :: struct { hwnd: HWND; update_interval_seconds: s32 = 3600; // 1 hour stop_event: HANDLE; is_test_mode: bool; } download_url :: (url: string) -> string, bool, string { log_info("Downloading config from URL: %\n", url); hInternet := InternetOpenW( utf8_to_wide("SingboxTrayUpdater"), INTERNET_OPEN_TYPE_PRECONFIG, null, null, 0 ); if !hInternet { return "", false, "Failed to initialize WinINet session"; } defer InternetCloseHandle(hInternet); flags: DWORD = INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE; if starts_with(url, "https") { flags |= INTERNET_FLAG_SECURE; } hUrl := InternetOpenUrlW( hInternet, utf8_to_wide(url), null, 0, flags, 0 ); if !hUrl { err := GetLastError(); return "", false, tprint("Failed to open URL (Win32 Error: %)", err); } defer InternetCloseHandle(hUrl); // Query response code 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 "", false, tprint("HTTP request failed with status code %", response_code); } } else { err := GetLastError(); return "", false, tprint("Failed to query HTTP status code (Win32 Error: %)", err); } 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 { err := GetLastError(); return "", false, tprint("Error reading from URL (Win32 Error: %)", err); } if bytes_read == 0 { break; } append(*builder, buffer.data, xx bytes_read); } result_str := builder_to_string(*builder); log_info("Configuration downloaded successfully (% bytes).\n", result_str.count); return result_str, true, ""; } perform_update :: (is_test_mode := false) -> changed: bool, success: bool, error_msg: string { config_filename := global_config_path; config_data, read_ok := read_entire_file(config_filename); if !read_ok { err := tprint("% does not exist. Please configure the Config URL first.", config_filename); return false, false, err; } defer free(config_data); url := get_json_string_field(config_data, "\"_url\""); if !url { err := tprint("No Config URL configured in %. Please configure the Config URL first.", config_filename); return false, false, err; } mode := get_json_string_field(config_data, "\"_mode\""); if !mode mode = "system_proxy"; port_str := get_json_val_field(config_data, "\"_port\""); port := 10801; if is_test_mode { port = 10899; // force test port } else if port_str { val, parse_ok, remainder := to_integer(port_str); if parse_ok { port = xx val; } } update_interval := 3600; interval_str := get_json_val_field(config_data, "\"_update_interval\""); if interval_str { val, parse_ok, remainder := to_integer(interval_str); if parse_ok { update_interval = xx val; } } downloaded, success, err_msg := download_url(url); if !success return false, false, err_msg; defer free(downloaded); modified := modify_config_inbounds(downloaded, port); defer free(modified); final_config := set_json_metadata(modified, url, mode, port, update_interval); defer free(final_config); if config_data == final_config { return false, true, ""; } write_ok := write_entire_file(config_filename, final_config); if !write_ok { err := tprint("Failed to write updated content to %", config_filename); return false, false, err; } return true, true, ""; } updater_thread_proc :: (thread: *Thread) -> s64 { data := cast(*Updater_Thread_Data) thread.data; if !data return 1; elapsed_seconds := 0; while true { status := WaitForSingleObject(data.stop_event, 1000); if status == WAIT_OBJECT_0 { break; } interval := data.update_interval_seconds; if interval <= 0 { elapsed_seconds = 0; continue; } elapsed_seconds += 1; if elapsed_seconds >= interval { elapsed_seconds = 0; log_debug("Background update: Starting automatic config check...\n"); changed, success, err_msg := perform_update(data.is_test_mode); if success { if changed { log_info("Background update: Configuration updated successfully. Triggering sing-box restart...\n"); if data.hwnd { PostMessageW(data.hwnd, WM_RESTART_SINGBOX, 0, 0); } } else { log_debug("Background update: Configuration is already up-to-date.\n"); } } else { log_error("Background update failed: %\n", err_msg); } } } return 0; } modify_config_inbounds :: (json_content: string, port: int) -> string { // Find "inbounds" key idx := find_index_from_left(json_content, "\"inbounds\""); if idx == -1 { first_brace := find_index_from_left(json_content, #char "{"); if first_brace == -1 return copy_string(json_content); builder: String_Builder; append(*builder, slice(json_content, 0, first_brace + 1)); append(*builder, tprint("\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": %\n }\n ],", port)); append(*builder, slice(json_content, first_brace + 1, json_content.count - (first_brace + 1))); return builder_to_string(*builder); } // Find the opening bracket '[' after "inbounds" search_start := idx + 10; open_bracket_idx := -1; for i: search_start..json_content.count-1 { if json_content[i] == #char "[" { open_bracket_idx = i; break; } else if json_content[i] == #char ":" || is_space(json_content[i]) { continue; } else { break; } } if open_bracket_idx == -1 return copy_string(json_content); // Find the matching closing bracket ']' close_bracket_idx := -1; bracket_depth := 0; for i: open_bracket_idx..json_content.count-1 { if json_content[i] == #char "[" { bracket_depth += 1; } else if json_content[i] == #char "]" { bracket_depth -= 1; if bracket_depth == 0 { close_bracket_idx = i; break; } } } if close_bracket_idx == -1 return copy_string(json_content); builder: String_Builder; append(*builder, slice(json_content, 0, open_bracket_idx)); append(*builder, tprint("[\n {\n \"type\": \"mixed\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": %\n }\n ]", port)); append(*builder, slice(json_content, close_bracket_idx + 1, json_content.count - (close_bracket_idx + 1))); return builder_to_string(*builder); } get_json_string_field :: (json: string, field: string) -> string { idx := find_index_from_left(json, field); if idx == -1 return ""; search_start := idx + field.count; open_quote := -1; for i: search_start..json.count-1 { if json[i] == #char "\"" { open_quote = i; break; } else if json[i] == #char ":" || is_space(json[i]) { continue; } else { break; } } if open_quote == -1 return ""; close_quote := -1; for i: open_quote+1..json.count-1 { if json[i] == #char "\"" && json[i-1] != #char "\\" { close_quote = i; break; } } if close_quote == -1 return ""; return slice(json, open_quote + 1, close_quote - open_quote - 1); } get_json_val_field :: (json: string, field: string) -> string { idx := find_index_from_left(json, field); if idx == -1 return ""; search_start := idx + field.count; val_start := -1; for i: search_start..json.count-1 { if json[i] == #char ":" || is_space(json[i]) { continue; } else { val_start = i; break; } } if val_start == -1 return ""; val_end := val_start; while val_end < json.count - 1 { next_char := json[val_end + 1]; if next_char == #char "," || next_char == #char "\n" || next_char == #char "\r" || next_char == #char "}" || next_char == #char "]" { break; } val_end += 1; } return trim(slice(json, val_start, val_end - val_start + 1)); } set_json_metadata :: (json: string, url: string, mode: string, port: int, update_interval: int) -> string { lines := split(json, "\n"); defer array_free(lines); builder: String_Builder; inserted := false; for lines { trimmed := trim(it); if find_index_from_left(trimmed, "\"_url\"") != -1 continue; if find_index_from_left(trimmed, "\"_mode\"") != -1 continue; if find_index_from_left(trimmed, "\"_port\"") != -1 continue; if find_index_from_left(trimmed, "\"_update_interval\"") != -1 continue; append(*builder, it); append(*builder, "\n"); if !inserted && find_index_from_left(trimmed, "{") != -1 { append(*builder, tprint(" \"_url\": \"%\",\n \"_mode\": \"%\",\n \"_port\": %,\n \"_update_interval\": %,\n", url, mode, port, update_interval)); inserted = true; } } return builder_to_string(*builder); } strip_json_metadata :: (json: string) -> string { lines := split(json, "\n"); defer array_free(lines); builder: String_Builder; for lines { trimmed := trim(it); if find_index_from_left(trimmed, "\"_url\"") != -1 continue; if find_index_from_left(trimmed, "\"_mode\"") != -1 continue; if find_index_from_left(trimmed, "\"_port\"") != -1 continue; if find_index_from_left(trimmed, "\"_update_interval\"") != -1 continue; append(*builder, it); append(*builder, "\n"); } return builder_to_string(*builder); }