sing-box-tray/updater.jai

298 lines
8.9 KiB
Plaintext

#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;
}
download_url :: (url: string) -> string, bool, string {
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);
}
return builder_to_string(*builder), true, "";
}
perform_update :: () -> changed: bool, success: bool, error_msg: string {
config_data, read_ok := read_entire_file("config.json");
if !read_ok return false, false, "config.json does not exist. Please configure the Config URL first.";
defer free(config_data);
url := get_json_string_field(config_data, "\"_url\"");
if !url return false, false, "No Config URL configured in config.json. Please configure the Config URL first.";
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 port_str {
val, parse_ok, remainder := to_integer(port_str);
if parse_ok {
port = 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);
defer free(final_config);
if config_data == final_config {
return false, true, "";
}
write_ok := write_entire_file("config.json", final_config);
if !write_ok {
return false, false, "Failed to write updated content to config.json";
}
return true, true, "";
}
updater_thread_proc :: (thread: *Thread) -> s64 {
data := cast(*Updater_Thread_Data) thread.data;
if !data return 1;
while true {
// Wait on the stop event for the specified interval.
// If the stop event is signaled, wake up immediately and terminate.
status := WaitForSingleObject(data.stop_event, cast(DWORD) (data.update_interval_seconds * 1000));
if status == WAIT_OBJECT_0 {
break;
}
changed, success, err_msg := perform_update();
if success && changed {
if data.hwnd {
PostMessageW(data.hwnd, WM_RESTART_SINGBOX, 0, 0);
}
}
}
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) -> 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;
append(*builder, it);
append(*builder, "\n");
if !inserted && find_index_from_left(trimmed, "{") != -1 {
append(*builder, tprint(" \"_url\": \"%\",\n \"_mode\": \"%\",\n \"_port\": %,\n", url, mode, port));
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;
append(*builder, it);
append(*builder, "\n");
}
return builder_to_string(*builder);
}