feat: add sing-box updater

This commit is contained in:
Ixniy Evonniy 2026-07-01 11:15:46 +03:00
parent b011fcb346
commit 7e26b24030
4 changed files with 440 additions and 68 deletions

View File

@ -533,7 +533,16 @@ download_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -
if state.thread_data.done {
KillTimer(hwnd, 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);
// 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);
}
}
@ -581,8 +590,8 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str
dialog_y := (screen_h - dialog_h) / 2;
state: Download_Dialog_State;
state.thread_data.url = url;
state.thread_data.dest_path = dest_path;
state.thread_data.url = copy_string(url);
state.thread_data.dest_path = copy_string(dest_path);
dialog_hwnd := CreateWindowExW(
WS_EX_DLGMODALFRAME,
@ -596,7 +605,11 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str
*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 {
EnableWindow(parent_hwnd, FALSE);

471
main.jai
View File

@ -111,6 +111,11 @@ App_State :: struct {
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 {
@ -457,11 +462,6 @@ get_last_singbox_logs_string :: () -> 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);
find_data: WIN32_FIND_DATAW;
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;
}
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;
@ -601,12 +906,21 @@ start_singbox :: (app: *App_State) -> bool {
// Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error
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);
free(config_data);
free(modified);
free(clean_config);
free(final_config);
}
}
@ -614,60 +928,8 @@ start_singbox :: (app: *App_State) -> bool {
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 {
clear_last_singbox_logs();
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);
installed := install_or_update_singbox_core(app, false);
return installed; // install_or_update_singbox_core handles starting sing-box
} else {
return false;
}
@ -742,6 +1004,7 @@ stop_singbox :: (app: *App_State) {
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);
@ -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) {
hMenu := CreatePopupMenu();
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, 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"));
@ -1019,6 +1335,9 @@ show_context_menu :: (app: *App_State) {
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");
@ -1265,6 +1584,15 @@ main :: () {
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);
@ -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.cbSize = size_of(NOTIFYICONDATAW);
nid.hWnd = hwnd;
@ -1398,6 +1735,18 @@ main :: () {
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);

View File

@ -15,6 +15,12 @@ Updater_Thread_Data :: struct {
download_url :: (url: string) -> string, bool, string {
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(
utf8_to_wide("SingboxTrayUpdater"),
INTERNET_OPEN_TYPE_PRECONFIG,
@ -32,11 +38,12 @@ download_url :: (url: string) -> string, bool, string {
flags |= INTERNET_FLAG_SECURE;
}
headers := "User-Agent: SingboxTray\r\n";
hUrl := InternetOpenUrlW(
hInternet,
utf8_to_wide(url),
null,
0,
utf8_to_wide(headers),
cast(u32) headers.count,
flags,
0
);
@ -361,11 +368,12 @@ download_file :: (url: string, dest_path: string) -> bool, string {
flags |= INTERNET_FLAG_SECURE;
}
headers := "User-Agent: SingboxTray\r\n";
hUrl := InternetOpenUrlW(
hInternet,
utf8_to_wide(url),
null,
0,
utf8_to_wide(headers),
cast(u32) headers.count,
flags,
0
);

View File

@ -165,6 +165,8 @@ CMD_UPDATE_DAILY :: 1015;
CMD_UPDATE_3D :: 1016;
CMD_UPDATE_WEEKLY :: 1017;
CMD_CONFIG_DIR :: 1018;
CMD_UPDATE_CORE :: 1019;
// Boolean constants
FALSE :: 0;