2728 lines
91 KiB
C++
2728 lines
91 KiB
C++
// #TODO: #OS_Win32
|
|
// [ ] #Exception handling code in `Win32_Exception_Filter`
|
|
// [~] #Thread cleanup: in `thread_deinit` is there any requirement to cleanup child threads?
|
|
// - I think: no? Threads should handle their own lifetimes, and the parent threads should ensure child threads are complete
|
|
// before terminating.
|
|
// Or we can move child threads up to the parent?
|
|
|
|
constexpr s64 FILETIME_TO_UNIX = 116444736000000000i64;
|
|
constexpr u64 Win32_Max_Path_Length = 260;
|
|
|
|
f64 GetUnixTimestamp () {
|
|
FILETIME fileTime;
|
|
GetSystemTimePreciseAsFileTime(&fileTime);
|
|
s64 ticks = ((s64)fileTime.dwHighDateTime << (s64)32) | (s64)fileTime.dwLowDateTime;
|
|
return (ticks - FILETIME_TO_UNIX) / (10.0 * 1000.0 * 1000.0);
|
|
}
|
|
s64 GetUnixTimestampNanoseconds () {
|
|
FILETIME fileTime;
|
|
GetSystemTimePreciseAsFileTime(&fileTime);
|
|
|
|
s64 ticks = ((s64)fileTime.dwHighDateTime << (s64)32)
|
|
| (s64)fileTime.dwLowDateTime; // in 100ns ticks
|
|
s64 unix_time = (ticks - FILETIME_TO_UNIX); // in 100ns ticks
|
|
|
|
s64 unix_time_nanoseconds = unix_time * 100;
|
|
|
|
return unix_time_nanoseconds;
|
|
}
|
|
|
|
u64 FILETIME_to_ticks (FILETIME fileTime) {
|
|
u64 ticks = ((u64)fileTime.dwHighDateTime << (u64)32)
|
|
| (u64)fileTime.dwLowDateTime; // in 100ns ticks
|
|
return ticks;
|
|
}
|
|
|
|
FILETIME u64_to_FILETIME (u64 time_u64) {
|
|
static_assert(sizeof(FILETIME) == sizeof(time_u64));
|
|
FILETIME ft;
|
|
memcpy(&ft, &time_u64, sizeof(FILETIME));
|
|
return ft;
|
|
}
|
|
|
|
string format_time_datetime (FILETIME ft) {
|
|
SYSTEMTIME stUTC, st;
|
|
FileTimeToSystemTime(&ft, &stUTC);
|
|
SystemTimeToTzSpecificLocalTime(nullptr, &stUTC, &st);
|
|
|
|
return format_string("%04u-%02u-%02u %02u:%02u:%02u.%03u",
|
|
st.wYear,
|
|
st.wMonth,
|
|
st.wDay,
|
|
st.wHour,
|
|
st.wMinute,
|
|
st.wSecond,
|
|
st.wMilliseconds);
|
|
}
|
|
|
|
string format_filetime_as_datetime (FILETIME ft, bool use_24_hour_time=false) {
|
|
char* MONTH_NAMES[12] = { "January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December" };
|
|
SYSTEMTIME stUTC, st;
|
|
bool result = (bool)FileTimeToSystemTime(&ft, &stUTC);
|
|
if (!result) {
|
|
log_error("[format_filetime_as_datetime] FileTimeToSystemTime failed!");
|
|
os_log_error();
|
|
return "";
|
|
}
|
|
result = (bool)SystemTimeToTzSpecificLocalTime(nullptr, &stUTC, &st);
|
|
if (!result) {
|
|
log_error("[format_filetime_as_datetime] SystemTimeToTzSpecificLocalTime failed!");
|
|
os_log_error();
|
|
return "";
|
|
}
|
|
|
|
char* month = MONTH_NAMES[st.wMonth - 1];
|
|
|
|
if (use_24_hour_time) {
|
|
return format_string("%u %s %u, %02u:%02u:%02u",
|
|
st.wDay,
|
|
month,
|
|
st.wYear,
|
|
st.wHour,
|
|
st.wMinute,
|
|
st.wSecond);
|
|
} else {
|
|
u16 hour12 = st.wHour % 12;
|
|
if (hour12 == 0) hour12 = 12;
|
|
|
|
char* am_pm = (st.wHour < 12) ? "AM" : "PM";
|
|
|
|
return format_string("%u %s %u, %02u:%02u:%02u %s",
|
|
st.wDay,
|
|
month,
|
|
st.wYear,
|
|
hour12,
|
|
st.wMinute,
|
|
st.wSecond,
|
|
am_pm);
|
|
}
|
|
}
|
|
|
|
struct OS_System_Info {
|
|
// #cpuid
|
|
s32 logical_processor_count;
|
|
s32 physical_core_count;
|
|
s32 primary_core_count;
|
|
s32 secondary_core_count; // Any weaker or "Efficiency" cores.
|
|
u64 page_size;
|
|
u64 large_page_size;
|
|
u64 allocation_granularity;
|
|
string machine_name;
|
|
|
|
// #Monitors
|
|
b32 monitors_enumerated;
|
|
Array<Monitor> monitors; // Back with default_allocator
|
|
|
|
// #Drives
|
|
Arena* drive_arena;
|
|
Table<string, OS_Drive*> drives; // should we just store ptrs to OS_Drive? I think so..
|
|
// That way we can fetch the OS_Drive* and have the pointer be stable. Especially if it's b
|
|
// backed by an arena.
|
|
};
|
|
|
|
enum class Open_Directories_In: s32 {
|
|
Explorer = 0,
|
|
FPilot = 1
|
|
};
|
|
|
|
struct OS_Process_Info {
|
|
u32 process_id;
|
|
b32 large_pages_allowed;
|
|
string binary_path;
|
|
string working_path;
|
|
string user_program_data_path;
|
|
Array<string> module_load_paths;
|
|
Array<string> environment_paths;
|
|
|
|
b32 window_class_initialized;
|
|
Open_Directories_In open_directories_in = Open_Directories_In::FPilot;
|
|
Arena* event_arena;
|
|
Array<Window_Info> windows;
|
|
};
|
|
|
|
struct OS_State_Win32 {
|
|
Arena* arena;
|
|
|
|
OS_System_Info system_info;
|
|
OS_Process_Info process_info;
|
|
};
|
|
|
|
global OS_State_Win32 global_win32_state;
|
|
internal b32 global_win32_is_quiet = 0; // No console output (`quiet` flag passed)
|
|
|
|
// Cached path data: Kinda yuck, but I want these to be globally available.
|
|
// #TODO: Should be part of Win32 global state, and we should NOT access without
|
|
// a mutex. Also, should be loaded once on startup.
|
|
global string PATH;
|
|
global ArrayView<string> PATH_split; // views into PATH.
|
|
|
|
void win32_reload_PATH () {
|
|
push_allocator_label("win32_PATH");
|
|
push_allocator(default_allocator()); // we want to keep PATH and file data around!
|
|
string_free(PATH);
|
|
array_free(PATH_split);
|
|
|
|
u32 PATH_length = GetEnvironmentVariableA("PATH", nullptr, 0);
|
|
ArrayView<u8> result = ArrayView<u8>(PATH_length);
|
|
GetEnvironmentVariableA("PATH", (LPSTR)result.data, PATH_length);
|
|
|
|
PATH = to_string(result);
|
|
PATH_split = string_split(PATH, ';');
|
|
}
|
|
|
|
void win32_delete_cached_PATH () {
|
|
push_allocator(default_allocator()); // we want to keep PATH and file data around!
|
|
string_free(PATH);
|
|
array_free(PATH_split);
|
|
}
|
|
|
|
Table<string, OS_Drive*>* get_drive_table () {
|
|
return &global_win32_state.system_info.drives;
|
|
}
|
|
|
|
OS_Drive* get_drive_from_label (string drive_label) {
|
|
// #TODO: Validate the input is in the correct format. We're looking for something like `C:\`
|
|
// not just the drive letter!
|
|
Table<string, OS_Drive*>* drive_table = get_drive_table();
|
|
OS_Drive** drive_ptr = table_find_pointer(drive_table, drive_label);
|
|
Assert(drive_ptr != nullptr);
|
|
return *drive_ptr;
|
|
}
|
|
|
|
ArrayView<OS_Drive*> os_get_available_drives () {
|
|
// #TODO: Maybe have a version of this API that sorts the drives before returning.
|
|
// @Allocates: Recommended to set context allocator to temp().
|
|
auto drive_table = get_drive_table();
|
|
Array<OS_Drive*> drives;
|
|
|
|
// #hash_table_iterator : instead of writing this everywhere, just use this function
|
|
// to get an Array of drives.
|
|
for (s64 i = 0; i < drive_table->allocated; i += 1) {
|
|
Table_Entry<string, OS_Drive*>* entry = &drive_table->entries[i]; // we should take ptr here if we want to modify?
|
|
|
|
if (entry->hash > HASH_TABLE_FIRST_VALID_HASH) {
|
|
if (entry->value->label.data == nullptr || !entry->value->is_present) continue; // Some volumes may not be real and therefore have no label.
|
|
|
|
array_add(drives, entry->value);
|
|
}
|
|
}
|
|
|
|
return drives;
|
|
}
|
|
|
|
ArrayView<OS_Drive*> os_get_available_drives_sorted () {
|
|
Allocator ctx_allocator = context_allocator();
|
|
push_allocator(temp());
|
|
|
|
auto drives = os_get_available_drives();
|
|
|
|
Array<string> drive_letters;
|
|
for_each(d, drives) {
|
|
array_add(drive_letters, copy_string(drives[d]->label));
|
|
}
|
|
|
|
qsort(drive_letters.data, drive_letters.count, sizeof(string), string_lexicographical_compare);
|
|
|
|
Array<OS_Drive*> drives_sorted = Array<OS_Drive*>(ctx_allocator);
|
|
auto drive_table = get_drive_table();
|
|
for_each(d, drive_letters) {
|
|
string drive_label = drive_letters[d];
|
|
|
|
array_add(drives_sorted, get_drive_from_label(drive_label));
|
|
}
|
|
|
|
return drives_sorted;
|
|
}
|
|
|
|
internal LONG WINAPI Win32_Exception_Filter (EXCEPTION_POINTERS* exception_ptrs) {
|
|
if (global_win32_is_quiet) { ExitProcess(1); }
|
|
|
|
local_persist volatile LONG first = 0;
|
|
if(InterlockedCompareExchange(&first, 1, 0) != 0)
|
|
{ // prevent failures in other threads to popup same message box
|
|
// this handler just shows first thread that crashes
|
|
// we are terminating afterwards anyway
|
|
for (;;) Sleep(1000);
|
|
}
|
|
|
|
// #TODO: Runtime assertion failed?
|
|
|
|
// #Exception handling code (TODO)
|
|
#if ENABLE_STACK_TRACE
|
|
if (thread_context()->stack_trace) {
|
|
os_write_string_unsynchronized("\n[Win32_Exception_Filter] Stack Trace\n", true);
|
|
print_stack_trace();
|
|
}
|
|
#endif
|
|
|
|
ExitProcess(1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// internal void Main_Entry_Point (int argc, WCHAR **argv);
|
|
internal void Win32_Entry_Point (int argc, WCHAR **argv) { stack_trace();
|
|
// #testing printing stack trace (unfinished).
|
|
// os_write_string_unsynchronized("Fatal Error!\n\nStack trace: ", true);
|
|
// print_stack_trace();
|
|
|
|
// Timed_Block_Print("Win32_Entry_Point");
|
|
// See: w32_entry_point_caller(); (raddebugger)
|
|
SetUnhandledExceptionFilter(&Win32_Exception_Filter);
|
|
|
|
SYSTEM_INFO sysinfo = {0};
|
|
GetSystemInfo(&sysinfo);
|
|
|
|
// Try to allow large pages if we can.
|
|
// b32 large_pages_allowed = 0;
|
|
// {
|
|
// HANDLE token;
|
|
// if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token))
|
|
// {
|
|
// LUID luid;
|
|
// if(LookupPrivilegeValue(0, SE_LOCK_MEMORY_NAME, &luid))
|
|
// {
|
|
// TOKEN_PRIVILEGES priv;
|
|
// priv.PrivilegeCount = 1;
|
|
// priv.Privileges[0].Luid = luid;
|
|
// priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
// large_pages_allowed = !!AdjustTokenPrivileges(token, 0, &priv, sizeof(priv), 0, 0);
|
|
// }
|
|
// CloseHandle(token);
|
|
// }
|
|
// }
|
|
|
|
push_arena(thread_context()->arena);
|
|
|
|
{ OS_System_Info* info = &global_win32_state.system_info;
|
|
info->logical_processor_count = (s32)sysinfo.dwNumberOfProcessors;
|
|
info->page_size = sysinfo.dwPageSize;
|
|
info->large_page_size = GetLargePageMinimum();
|
|
info->allocation_granularity = sysinfo.dwAllocationGranularity;
|
|
}
|
|
{ OS_Process_Info* info = &global_win32_state.process_info;
|
|
info->large_pages_allowed = false;
|
|
info->process_id = GetCurrentProcessId();
|
|
}
|
|
// #cpuid
|
|
{ OS_System_Info* info = &global_win32_state.system_info;
|
|
u32 length = 0;
|
|
GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, (PDWORD)&length);
|
|
u8* cpu_information_buffer = NewArray<u8>(length);
|
|
GetLogicalProcessorInformationEx(RelationProcessorCore, // *sigh*
|
|
(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)cpu_information_buffer, (PDWORD)&length);
|
|
|
|
u32 offset = 0;
|
|
|
|
u32 all_cpus_count = 0; // all logical cpus
|
|
u32 physical_cpu_count = 0;
|
|
u32 max_performance = 0;
|
|
u32 performance_core_count = 0;
|
|
|
|
while (offset < length) {
|
|
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* cpu_information
|
|
= (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)(cpu_information_buffer + offset);
|
|
|
|
offset += cpu_information->Size;
|
|
|
|
u32 count_per_group_physical = 1;
|
|
u32 value = (u32)cpu_information->Processor.GroupMask->Mask;
|
|
u32 count_per_group = __popcnt(value); // logical
|
|
if (cpu_information->Relationship != RelationProcessorCore) continue;
|
|
|
|
if (cpu_information->Processor.EfficiencyClass > max_performance) {
|
|
max_performance = cpu_information->Processor.EfficiencyClass;
|
|
performance_core_count = count_per_group_physical;
|
|
} else if (cpu_information->Processor.EfficiencyClass == max_performance) {
|
|
performance_core_count += count_per_group_physical;
|
|
}
|
|
|
|
physical_cpu_count += count_per_group_physical;
|
|
all_cpus_count += count_per_group;
|
|
}
|
|
|
|
info->physical_core_count = (s32)physical_cpu_count;
|
|
info->primary_core_count = (s32)performance_core_count;
|
|
info->secondary_core_count = info->physical_core_count - info->primary_core_count;
|
|
}
|
|
|
|
{ OS_System_Info* info = &global_win32_state.system_info;
|
|
info->monitors.allocator = default_allocator();
|
|
u8 buffer[MAX_COMPUTERNAME_LENGTH + 1] = {0};
|
|
DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
|
|
if(GetComputerNameA((char*)buffer, &size)) {
|
|
string machine_name_temp = string(size, buffer);
|
|
info->machine_name = copy_string(machine_name_temp);
|
|
}
|
|
}
|
|
|
|
{ OS_Process_Info* info = &global_win32_state.process_info;
|
|
info->windows.allocator = default_allocator();
|
|
DWORD length = GetCurrentDirectoryW(0, 0);
|
|
// This can be freed later when we call temp_reset();
|
|
u16* memory = NewArray<u16>(temp(), length + 1);
|
|
length = GetCurrentDirectoryW(length + 1, (WCHAR*)memory);
|
|
info->working_path = wide_to_utf8(memory, length);
|
|
Assert(is_valid(info->working_path));
|
|
}
|
|
|
|
// Setup event arena, allocators for Array<> types.
|
|
if (!global_win32_state.process_info.event_arena) {
|
|
global_win32_state.process_info.event_arena = bootstrap_arena(Arena_Reserve::Size_64K, "global_win32_state.process_info.event_arena");
|
|
}
|
|
|
|
// [ ] Get Working directory (info->working_path)
|
|
// [ ] GetEnvironmentStringsW
|
|
temp_reset();
|
|
}
|
|
|
|
C_LINKAGE DWORD OS_Windows_Thread_Entry_Point (void* parameter) {
|
|
Thread* thread = (Thread*)parameter;
|
|
|
|
set_thread_context(thread->context);
|
|
|
|
DWORD result = (DWORD)thread->proc(thread);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Individual Thread API
|
|
#define thread_task(T) (T*)thread->data
|
|
#define thread_group_task(T) (T*)work
|
|
|
|
internal bool thread_init (Thread* thread, Thread_Proc proc, string thread_name) {
|
|
Assert(thread != nullptr && proc != nullptr);
|
|
|
|
DWORD windows_thread_id = 0;
|
|
|
|
HANDLE windows_thread = CreateThread(nullptr, 0, OS_Windows_Thread_Entry_Point,
|
|
thread, CREATE_SUSPENDED, &windows_thread_id);
|
|
|
|
if (windows_thread == 0 || windows_thread == INVALID_HANDLE_VALUE) {
|
|
return false;
|
|
}
|
|
|
|
s64 this_thread_index = InterlockedIncrement(&next_thread_index);
|
|
// 2. #NewContext Setup NEW thread local context
|
|
string arena_ex_label;
|
|
string temp_label;
|
|
string error_arena_label;
|
|
string allocator_label;
|
|
string log_builder_label;
|
|
string string_builder_label;
|
|
|
|
if (is_valid(thread_name)) {
|
|
arena_ex_label = format_string(temp(), "%s Arena", thread_name.data);
|
|
temp_label = format_string(temp(), "%s Temp", thread_name.data);
|
|
error_arena_label = format_string(temp(), "%s Error Arena", thread_name.data);
|
|
allocator_label = format_string(temp(), "%s Initialization", thread_name.data);
|
|
log_builder_label = format_string(temp(), "%s Log Builder", thread_name.data);
|
|
string_builder_label = format_string(temp(), "%s String Builder", thread_name.data);
|
|
} else {
|
|
arena_ex_label = format_string(temp(), "Thread %lld Arena", this_thread_index);
|
|
temp_label = format_string(temp(), "Thread %lld Temp", this_thread_index);
|
|
error_arena_label = format_string(temp(), "Thread %lld Error Arena", this_thread_index);
|
|
allocator_label = format_string(temp(), "Thread %lld Initialization", this_thread_index);
|
|
log_builder_label = format_string(temp(), "Thread %lld Log Builder", this_thread_index);
|
|
string_builder_label = format_string(temp(), "Thread %lld String Builder", this_thread_index);
|
|
}
|
|
|
|
push_allocator_label(allocator_label);
|
|
|
|
ExpandableArena* arena_ex = bootstrap_expandable_arena(Arena_Reserve::Size_64M, arena_ex_label);
|
|
push_arena(arena_ex);
|
|
|
|
// #NOTE: we don't assign thread_local_context until we hit the #thread_entry_point
|
|
thread->context = New<Thread_Context>();
|
|
thread->context->temp = bootstrap_expandable_arena(Arena_Reserve::Size_2M, temp_label);
|
|
thread->context->arena = arena_ex;
|
|
thread->context->allocator = allocator(arena_ex);
|
|
thread->context->thread_idx = (s32)this_thread_index;
|
|
// #NOTE: This will disappear once the thread is de-initted. If we want this string, copy it!
|
|
thread->context->thread_name = copy_string(thread_name);
|
|
thread->context->log_builder = new_string_builder(Arena_Reserve::Size_64M, log_builder_label);
|
|
thread->context->string_builder = new_string_builder(Arena_Reserve::Size_2M, string_builder_label);
|
|
thread->context->error_arena = bootstrap_arena(Arena_Reserve::Size_64M, error_arena_label);
|
|
thread->context->logger = {default_logger_proc, &default_logger};
|
|
|
|
thread->context->parent_thread_context = thread_context();
|
|
|
|
thread->os_thread.windows_thread = windows_thread;
|
|
thread->os_thread.windows_thread_id = windows_thread_id;
|
|
|
|
thread->proc = proc;
|
|
thread->index = this_thread_index;
|
|
|
|
thread_context()->child_threads.allocator = allocator(thread_context()->arena);
|
|
array_add(thread_context()->child_threads, thread);
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void thread_deinit (Thread* thread, bool zero_thread) {
|
|
// Move errors from thread to parent thread
|
|
push_errors_to_parent_thread(thread->context);
|
|
|
|
if (thread->os_thread.windows_thread) {
|
|
CloseHandle(thread->os_thread.windows_thread);
|
|
thread->os_thread.windows_thread = nullptr;
|
|
}
|
|
|
|
// remove from thread->context->parent_thread->child_threads
|
|
array_unordered_remove_by_value(thread->context->parent_thread_context->child_threads, thread, 1);
|
|
|
|
array_reset(*thread->context->log_builder);
|
|
free_string_builder(thread->context->log_builder);
|
|
free_string_builder(thread->context->string_builder);
|
|
arena_delete(thread->context->error_arena);
|
|
arena_delete(thread->context->temp);
|
|
arena_delete(thread->context->arena); // must come last because thread->context is allocated with this arena!
|
|
|
|
if (zero_thread) memset(thread, 0, sizeof(Thread));
|
|
}
|
|
|
|
internal void thread_start (Thread* thread, void* thread_data) {
|
|
if (thread_data) thread->data = thread_data;
|
|
ResumeThread(thread->os_thread.windows_thread);
|
|
}
|
|
|
|
internal bool thread_is_done (Thread* thread, s32 milliseconds) {
|
|
Assert(milliseconds >= -1);
|
|
|
|
DWORD result = WaitForSingleObject(thread->os_thread.windows_thread, (DWORD)milliseconds);
|
|
return result != WAIT_TIMEOUT;
|
|
}
|
|
|
|
// #thread_primitives
|
|
internal void mutex_init (Mutex* mutex) {
|
|
InitializeCriticalSection(&mutex->csection);
|
|
}
|
|
internal void mutex_destroy (Mutex* mutex) {
|
|
DeleteCriticalSection(&mutex->csection);
|
|
}
|
|
internal void lock (Mutex* mutex) {
|
|
EnterCriticalSection(&mutex->csection);
|
|
}
|
|
internal void unlock (Mutex* mutex) {
|
|
LeaveCriticalSection(&mutex->csection);
|
|
}
|
|
internal void semaphore_init (Semaphore* sem, s32 initial_value) {
|
|
Assert(initial_value >= 0);
|
|
sem->event = CreateSemaphoreW(nullptr, initial_value, 0x7fffffff, nullptr);
|
|
}
|
|
internal void semaphore_destroy (Semaphore* sem) {
|
|
CloseHandle(sem->event);
|
|
}
|
|
internal void signal (Semaphore* sem) {
|
|
ReleaseSemaphore(sem->event, 1, nullptr);
|
|
}
|
|
|
|
internal Wait_For_Result wait_for (Semaphore* sem, s32 milliseconds) {
|
|
DWORD res = 0;
|
|
if (milliseconds < 0) {
|
|
res = WaitForSingleObject(sem->event, INFINITE);
|
|
} else {
|
|
res = WaitForSingleObject(sem->event, (u32)milliseconds);
|
|
}
|
|
|
|
Assert(res != WAIT_FAILED);
|
|
|
|
if (res == WAIT_OBJECT_0) return Wait_For_Result::SUCCESS;
|
|
if (res == WAIT_TIMEOUT) return Wait_For_Result::TIMEOUT;
|
|
|
|
return Wait_For_Result::ERROR;
|
|
}
|
|
|
|
internal void condition_variable_init (Condition_Variable* cv) {
|
|
InitializeConditionVariable(&cv->condition_variable);
|
|
}
|
|
internal void condition_variable_destroy (Condition_Variable* cv) {
|
|
// No action required.
|
|
}
|
|
internal void wait (Condition_Variable* cv, Mutex* mutex, s32 wait_time_ms) {
|
|
SleepConditionVariableCS(&cv->condition_variable, &mutex->csection, (DWORD)wait_time_ms);
|
|
}
|
|
internal void wake (Condition_Variable* cv) {
|
|
WakeConditionVariable(&cv->condition_variable);
|
|
}
|
|
internal void wake_all (Condition_Variable* cv) {
|
|
WakeAllConditionVariable(&cv->condition_variable);
|
|
}
|
|
|
|
internal string get_error_string (OS_Error_Code error_code) {
|
|
u16* lpMsgBuf;
|
|
bool success = (bool)FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
|
|
(LPWSTR)&lpMsgBuf, 0, nullptr);
|
|
if (!success) { return ""; }
|
|
|
|
push_allocator(temp());
|
|
|
|
string result = wide_to_utf8(lpMsgBuf);
|
|
|
|
LocalFree(lpMsgBuf);
|
|
|
|
return trim_right(result, "\r\t\n");
|
|
}
|
|
|
|
internal void os_log_error () {
|
|
OS_Error_Code error_code = GetLastError();
|
|
log_error(" > GetLastError code: %d, %s", error_code, get_error_string(error_code).data);
|
|
}
|
|
|
|
internal bool file_is_valid (File file) {
|
|
if (file.handle == INVALID_HANDLE_VALUE) return false;
|
|
if (file.handle == 0) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
internal File file_open (string file_path, bool for_writing, bool keep_existing_content, bool log_errors) {
|
|
HANDLE handle;
|
|
|
|
push_allocator(temp()); // for utf8 -> wide conversions:
|
|
|
|
if (for_writing) {
|
|
u32 creation = (keep_existing_content) ? OPEN_ALWAYS : CREATE_ALWAYS;
|
|
handle = CreateFileW(
|
|
(LPCWSTR)utf8_to_wide(file_path).data,
|
|
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
|
|
FILE_SHARE_READ,
|
|
nullptr, creation, 0, nullptr);
|
|
} else {
|
|
u32 creation = OPEN_EXISTING;
|
|
handle = CreateFileW(
|
|
(LPCWSTR)utf8_to_wide(file_path).data,
|
|
FILE_GENERIC_READ,
|
|
FILE_SHARE_READ,
|
|
nullptr, creation, 0, nullptr);
|
|
}
|
|
|
|
if (handle == INVALID_HANDLE_VALUE && log_errors) {
|
|
// OS_Error_Code error_code = GetLastError();
|
|
log_error("Could not open file `%s`", copy_string(temp(), file_path).data);
|
|
os_log_error();
|
|
}
|
|
|
|
File file;
|
|
file.handle = handle;
|
|
|
|
return file;
|
|
}
|
|
|
|
internal void file_close (File* file) {
|
|
CloseHandle(file->handle);
|
|
}
|
|
|
|
internal bool file_read (File file, u8* data, s64 bytes_to_read_count, s64* bytes_read_count) {
|
|
// ignore bytes_read_count if null.
|
|
if (data == nullptr) {
|
|
log_error("file_read called with null destination pointer.");
|
|
os_log_error();
|
|
if (bytes_read_count) (*bytes_read_count) = 0;
|
|
return false;
|
|
}
|
|
if (bytes_to_read_count <= 0) {
|
|
if (bytes_read_count) (*bytes_read_count) = 0;
|
|
return false;
|
|
}
|
|
|
|
bool read_success = false;
|
|
s64 total_read = 0;
|
|
|
|
// loop to read more data than can be specified by the DWORD param ReadFile takes:
|
|
while (total_read < bytes_to_read_count) {
|
|
s64 remaining = bytes_to_read_count - total_read;
|
|
DWORD to_read;
|
|
if (remaining <= 0x7FFFFFFF) {
|
|
to_read = (DWORD)remaining;
|
|
} else {
|
|
to_read = 0x7FFFFFFF; // 2147483647 bytes ~2GB
|
|
}
|
|
|
|
DWORD single_read_length = 0;
|
|
read_success = (bool)ReadFile(file.handle, data + total_read, to_read, &single_read_length, nullptr);
|
|
|
|
total_read += single_read_length;
|
|
if (!read_success || single_read_length == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bytes_read_count) (*bytes_read_count) = total_read;
|
|
return read_success;
|
|
}
|
|
|
|
internal bool file_length (File file, s64* length) {
|
|
if (length == nullptr) {
|
|
log_error("Calling file_length with null `length` param!");
|
|
return false;
|
|
}
|
|
if (!file_is_valid(file)) { return false; }
|
|
s64 size;
|
|
bool success = (bool)GetFileSizeEx(file.handle, (PLARGE_INTEGER)&size);
|
|
|
|
(*length) = size;
|
|
|
|
return true;
|
|
}
|
|
|
|
internal bool file_length (string file_path, s64* length) {
|
|
if (length == nullptr) {
|
|
log_error("Calling file_length with null `length` param!");
|
|
return false;
|
|
}
|
|
File f = file_open(file_path);
|
|
|
|
if (!file_is_valid(f)) { return false; }
|
|
bool success = file_length(f, length);
|
|
|
|
file_close(&f);
|
|
|
|
return success;
|
|
}
|
|
|
|
internal s64 file_current_position (File file) {
|
|
constexpr s64 invalid_file_position = -1;
|
|
if (!file_is_valid(file)) { return invalid_file_position; }
|
|
s64 offset = 0;
|
|
LARGE_INTEGER liDistanceToMove;
|
|
bool result = (bool)SetFilePointerEx(file.handle, liDistanceToMove, (PLARGE_INTEGER)&offset, FILE_CURRENT);
|
|
if (!result) { return invalid_file_position; }
|
|
return (s64)offset;
|
|
}
|
|
|
|
internal bool file_set_position (File file, s64 position) {
|
|
if (!file_is_valid(file)) { return false; }
|
|
if (position < 0) { Assert(false); return false; }
|
|
|
|
LARGE_INTEGER position_li; position_li.QuadPart = position;
|
|
return (bool)SetFilePointerEx(file.handle, position_li, nullptr, FILE_BEGIN);
|
|
}
|
|
|
|
ArrayView<u8> read_entire_file (File file, bool add_null_terminator) {
|
|
ArrayView<u8> file_data;
|
|
|
|
bool result = file_length(file, &file_data.count);
|
|
if (!result) return {};
|
|
result = file_set_position(file, 0);
|
|
if (!result) return {};
|
|
|
|
s64 total_file_size = file_data.count;
|
|
|
|
if (add_null_terminator) {
|
|
total_file_size += 1;
|
|
}
|
|
|
|
file_data.data = NewArray<u8>(total_file_size, false);
|
|
if (file_data.data == nullptr) return {};
|
|
|
|
s64 bytes_read = 0;
|
|
result = file_read(file, file_data.data, file_data.count, &bytes_read);
|
|
if (!result) {
|
|
array_free(file_data);
|
|
return {};
|
|
}
|
|
|
|
if (add_null_terminator) {
|
|
file_data[total_file_size-1] = 0;
|
|
}
|
|
|
|
Assert(bytes_read == file_data.count);
|
|
file_data.count = bytes_read;
|
|
|
|
return file_data;
|
|
}
|
|
|
|
internal ArrayView<u8> read_entire_file (string file_path, bool add_null_terminator, bool log_errors) {
|
|
File f = file_open(file_path, false, false, log_errors);
|
|
if (!file_is_valid(f)) return {};
|
|
|
|
ArrayView<u8> file_data = read_entire_file(f, add_null_terminator);
|
|
|
|
file_close(&f);
|
|
|
|
return file_data;
|
|
}
|
|
|
|
internal bool file_write (File* file, void* data, s64 length) {
|
|
// @incomplete - deal with inputs > 32 bits (>2GB)
|
|
u32 length_u32 = (u32)length;
|
|
Assert(length == length_u32);
|
|
|
|
u32 bytes_written;
|
|
bool result = (bool)WriteFile(file->handle, data, length_u32, (LPDWORD)&bytes_written, nullptr);
|
|
|
|
return result;
|
|
}
|
|
|
|
force_inline bool file_write (File* file, ArrayView<u8> view) {
|
|
return file_write(file, view.data, view.count);
|
|
}
|
|
|
|
internal bool write_entire_file (string file_path, void* file_data, s64 count) {
|
|
File f = file_open(file_path, true, false);
|
|
if (!file_is_valid(f)) return false;
|
|
|
|
bool result = file_write(&f, file_data, count);
|
|
|
|
file_close(&f);
|
|
return result;
|
|
}
|
|
|
|
internal bool write_entire_file (string file_path, ArrayView<u8> file_data) {
|
|
return write_entire_file(file_path, file_data.data, file_data.count);
|
|
}
|
|
|
|
internal LRESULT // see os_w32_wnd_proc from raddebugger.
|
|
win32_wnd_proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
|
LRESULT result = 0;
|
|
bool good = true;
|
|
|
|
Arena* event_arena = global_win32_state.process_info.event_arena;
|
|
Assert(event_arena != nullptr);
|
|
|
|
switch (uMsg) {
|
|
default: {
|
|
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
|
|
} break;
|
|
case WM_CLOSE: {
|
|
ExitProcess(0); // #temp.
|
|
} break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal bool file_exists (string file_path) {
|
|
push_allocator(temp());
|
|
|
|
DWORD result = GetFileAttributesW((LPCWSTR)utf8_to_wide(file_path).data);
|
|
|
|
return (result != INVALID_FILE_ATTRIBUTES);
|
|
}
|
|
|
|
internal bool path_is_directory (string file_path, bool* success=nullptr) {
|
|
push_allocator(temp());
|
|
|
|
DWORD result = GetFileAttributesW((LPCWSTR)utf8_to_wide(file_path).data);
|
|
|
|
if (result == INVALID_FILE_ATTRIBUTES) {
|
|
if (success) (*success) = false;
|
|
return false;
|
|
}
|
|
|
|
if (success) (*success) = true;
|
|
return (result & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
}
|
|
|
|
internal bool win32_create_directory_internal (string path, SECURITY_ATTRIBUTES* attr=nullptr) {
|
|
const u64 Max_Directory_Path = Win32_Max_Path_Length - 12;
|
|
const string Long_Path_Prefix = "\\\\?\\";
|
|
|
|
push_allocator(temp()); // nothing we create here needs to stick around
|
|
|
|
wstring path_wide = utf8_to_wide(path);
|
|
|
|
// CreateDirectory does not support paths longer than Max_Directory_Path unless they start with the Long_Path_Prefix
|
|
if (!begins_with(path, Long_Path_Prefix)) {
|
|
u32 full_path_size = GetFullPathNameW((LPCWSTR)path_wide.data, 0, nullptr, nullptr);
|
|
if (full_path_size == 0) {
|
|
log_error("[win32_create_directory_internal] GetFullPathNameW failed with input %s", copy_string(path).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
if (full_path_size > Max_Directory_Path) {
|
|
s64 prefixed_full_path_size = full_path_size + Long_Path_Prefix.count; // size in wchar_t with the prefix
|
|
// This is a bit of a silly way to do this...
|
|
u16* full_path_wide = NewArray<u16>(2 * prefixed_full_path_size);
|
|
wstring long_path_prefix_wide = utf8_to_wide(Long_Path_Prefix);
|
|
memcpy(full_path_wide, long_path_prefix_wide.data, Long_Path_Prefix.count * 2);
|
|
u32 real_full_path_size = GetFullPathNameW((LPCWSTR)path_wide.data, full_path_size, (LPWSTR)full_path_wide + Long_Path_Prefix.count, nullptr);
|
|
if (!real_full_path_size) {
|
|
log_error("[win32_create_directory_internal] GetFullPathNameW failed on long-path inputs");
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
Assert(real_full_path_size <= full_path_size - 1); // -1 because on success, it does NOT include the null character, but on failure it does. This used to be ==, but in some cases we get a shorter path... I guess the return value of GetFullPathNameW is inexact in some cases!
|
|
|
|
path_wide = { real_full_path_size, full_path_wide };
|
|
}
|
|
}
|
|
|
|
bool success = CreateDirectoryW((LPCWSTR)path_wide.data, attr);
|
|
if (success) return true;
|
|
|
|
u32 error = GetLastError();
|
|
if (error == ERROR_ALREADY_EXISTS) return true;
|
|
|
|
log_error("[win32_create_directory_internal] Failed to create directory.");
|
|
os_log_error();
|
|
|
|
return false;
|
|
}
|
|
|
|
// I've always hated "mkdir, makedir" and any variations of it.
|
|
internal bool os_create_directory (string name, bool recursive=false) {
|
|
if (recursive) {
|
|
s64 start_index = 0;
|
|
#if OS_WINDOWS
|
|
// Windows doesn't allow creating a root directory (e.g. "X:" or "X:\\")
|
|
if (name.count > 3 && (name.data[1] == ':')) {
|
|
start_index = 3;
|
|
}
|
|
#endif
|
|
if (name.count > 0 && name.data[0] == '/' || name.data[0] == '\\') {
|
|
start_index = 1;
|
|
}
|
|
// #TODO: Find next index:
|
|
s64 index = find_index_from_left(name, '/', start_index);
|
|
#if OS_WINDOWS
|
|
if (index == -1) {
|
|
index = find_index_from_left(name, '\\', start_index);
|
|
}
|
|
#endif
|
|
while (index != -1) {
|
|
bool success = win32_create_directory_internal(string_view(name, 0, index));
|
|
if (!success) { return false; }
|
|
|
|
// Look for the next slash. If there are multiple slahes in a row, use the last one:
|
|
while (true) {
|
|
start_index = index + 1;
|
|
index = find_index_from_left(name, '/', start_index);
|
|
#if OS_WINDOWS
|
|
if (index == -1) {
|
|
index = find_index_from_left(name, '\\', start_index);
|
|
}
|
|
#endif
|
|
|
|
if (index != start_index) break;
|
|
}
|
|
}
|
|
|
|
if (start_index == name.count-1) {
|
|
// we already created innermost directory
|
|
return true;
|
|
}
|
|
} // if (recursive)
|
|
|
|
return win32_create_directory_internal(name);
|
|
}
|
|
|
|
internal BOOL
|
|
monitor_enum_proc (HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM data) {
|
|
Monitor monitor = {};
|
|
monitor.left = rect->left;
|
|
monitor.top = rect->top;
|
|
monitor.right = rect->right;
|
|
monitor.bottom = rect->bottom;
|
|
|
|
monitor.monitor_info.cbSize = sizeof(MONITORINFO);
|
|
|
|
GetMonitorInfoW(hMonitor, &monitor.monitor_info);
|
|
monitor.primary = !!(monitor.monitor_info.dwFlags & 0x1);
|
|
monitor.present = true;
|
|
|
|
push_allocator_label("global_win32_state.system_info.monitors");
|
|
array_add(global_win32_state.system_info.monitors, monitor);
|
|
|
|
return true;
|
|
}
|
|
|
|
// #TODO(Low Priority): how do I setup a callback if monitors configuration is changed?
|
|
// we handle WM_DISPLAYCHANGE or WM_DEVICECHANGE and call EnumDisplayMonitors again.
|
|
internal void os_enumerate_monitors () {
|
|
if (!global_win32_state.system_info.monitors_enumerated) {
|
|
// should reset array?
|
|
if (!EnumDisplayMonitors(nullptr, nullptr, monitor_enum_proc, 0)) {
|
|
log_fatal_error("Failed to enumerate monitors\n");
|
|
Assert(false); // Failed to enumerate monitors
|
|
ExitProcess(1);
|
|
}
|
|
|
|
global_win32_state.system_info.monitors_enumerated = true;
|
|
}
|
|
}
|
|
|
|
Window_Dimensions platform_get_centered_window_dimensions (bool open_on_largest_monitor=false) {
|
|
os_enumerate_monitors();
|
|
|
|
Assert(global_win32_state.system_info.monitors.count > 0); // must have at least 1 monitor!
|
|
|
|
Array<Monitor> monitors = global_win32_state.system_info.monitors;
|
|
Monitor monitor = monitors[0];
|
|
|
|
if (open_on_largest_monitor) {
|
|
s64 max_area = 0;
|
|
for (s64 i = 0; i < monitors.count; i += 1) {
|
|
s64 width = monitors[i].right - monitors[i].left;
|
|
s64 height = monitors[i].bottom - monitors[i].top;
|
|
s64 area = width * height;
|
|
|
|
if (max_area < area) {
|
|
monitor = monitors[i];
|
|
max_area = area;
|
|
} else if (max_area == area && monitors[i].primary) {
|
|
// if monitors are the same dimension, then just use primary monitor
|
|
monitor = monitors[i];
|
|
}
|
|
}
|
|
} else { // Opens on whatever monitor is marked as "primary."
|
|
for (s64 i = 0; i < monitors.count; i += 1) {
|
|
if (monitors[i].primary) {
|
|
monitor = monitors[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
s64 monitor_width = monitor.right - monitor.left;
|
|
s64 monitor_height = monitor.bottom - monitor.top;
|
|
|
|
s64 window_width = (s64)((f64)monitor_width / 1.125);
|
|
s64 window_height = (s64)((f64)monitor_height / 1.25);
|
|
s64 window_x = (monitor.left + (monitor_width / 2) - (window_width / 2));
|
|
s64 window_y = (monitor.top + (monitor_height / 2) - (window_height / 2));
|
|
|
|
Window_Dimensions dimensions = {
|
|
(s32)window_x,
|
|
(s32)window_y,
|
|
(s32)window_width,
|
|
(s32)window_height,
|
|
};
|
|
|
|
return dimensions;
|
|
}
|
|
|
|
bool Win32_Set_Main_Icon () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info->icon) {
|
|
HICON old_icon = (HICON)SendMessage(info->window, WM_SETICON, ICON_BIG, (LPARAM)info->icon);
|
|
if (old_icon) DestroyIcon(old_icon);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#define ICON_CONTEXT_MENU_ITEM_ID 5011
|
|
#define MAIN_WINDOW_TRAY_ICON_ID 5001
|
|
#define WM_TRAYICON WM_USER + 1 // our own value to identify when receiving a message.
|
|
|
|
bool Win32_Set_Tray_Icon (string tooltip_text) {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info->icon_minimized) {
|
|
push_allocator(temp());
|
|
wstring tooltip_text_wide = utf8_to_wide(tooltip_text);
|
|
Assert(tooltip_text_wide.count < 128);
|
|
|
|
info->nid.cbSize = sizeof(NOTIFYICONDATAW);
|
|
info->nid.hWnd = info->window;
|
|
info->nid.uID = MAIN_WINDOW_TRAY_ICON_ID;
|
|
info->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
|
info->nid.uCallbackMessage = WM_TRAYICON; // Custom message when interacting with the tray icon
|
|
info->nid.hIcon = info->icon_minimized;
|
|
memcpy(info->nid.szTip, tooltip_text_wide.data, tooltip_text_wide.count);
|
|
|
|
bool success = Shell_NotifyIconW(NIM_ADD, &info->nid);
|
|
if (success) {
|
|
info->tray_icon_added = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Win32_Show_Tray_Icon () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info && info->tray_icon_added) {
|
|
return Shell_NotifyIconW(NIM_ADD, &info->nid);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Win32_Hide_Tray_Icon () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info && info->tray_icon_added) {
|
|
return Shell_NotifyIconW(NIM_DELETE, &info->nid);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Win32_Hide_Window_Titlebar () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info && info->window) {
|
|
LONG style = GetWindowLongW(info->window, GWL_STYLE);
|
|
style &= (LONG)(~(WS_CAPTION | WS_SYSMENU));
|
|
SetWindowLongW(info->window, GWL_STYLE, style);
|
|
SetWindowPos(info->window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Win32_Show_Window_Titlebar () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
if (info && info->window) {
|
|
LONG style = GetWindowLongW(info->window, GWL_STYLE);
|
|
style |= (LONG)(WS_CAPTION | WS_SYSMENU);
|
|
SetWindowLongW(info->window, GWL_STYLE, style);
|
|
SetWindowPos(info->window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Win32_Minimize_Window_To_Tray (Window_Info* info) {
|
|
if (info && info->window) {
|
|
ShowWindow(info->window, SW_HIDE);
|
|
Win32_Show_Tray_Icon();
|
|
info->minimized_to_tray = true;
|
|
}
|
|
}
|
|
|
|
void Win32_Restore_Window_From_Tray (Window_Info* info) {
|
|
if (info && info->window && info->minimized_to_tray) {
|
|
ShowWindow(info->window, SW_RESTORE);
|
|
Win32_Hide_Tray_Icon();
|
|
info->minimized_to_tray = false;
|
|
}
|
|
}
|
|
|
|
// Win32_Restore_Window_From_Tray();
|
|
void Win32_Bring_Window_To_Foreground (Window_Info* info) {
|
|
Win32_Restore_Window_From_Tray(info);
|
|
if (info && info->window) {
|
|
if (os_window_is_minimized(info->window)) {
|
|
ShowWindow(info->window, SW_RESTORE);
|
|
}
|
|
SetForegroundWindow(info->window);
|
|
}
|
|
}
|
|
|
|
bool Win32_Load_Main_Window_Icon_Minimized (string icon_path) {
|
|
HICON result = (HICON)LoadImageW(nullptr, (LPCWSTR)utf8_to_wide(icon_path).data, IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
|
|
Window_Info* info = get_main_window_pointer();
|
|
info->icon_minimized = result;
|
|
|
|
return (result != nullptr);
|
|
}
|
|
|
|
bool Win32_Load_Main_Window_Icon (string icon_path) {
|
|
HICON result = (HICON)LoadImageW(nullptr, (LPCWSTR)utf8_to_wide(icon_path).data, IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
|
|
Window_Info* info = get_main_window_pointer();
|
|
info->icon = result;
|
|
|
|
return (result != nullptr);
|
|
}
|
|
|
|
// #window_creation -> put API in OS_Win32.h
|
|
// Instead of returning WindowType, return the handle + other information.
|
|
bool os_create_window (string new_window_name, Window_Type parent, bool center_window, bool open_on_largest_monitor, bool display_window, void* wnd_proc_override) {
|
|
local_persist string class_name = "Win32_Window_Class";
|
|
|
|
WNDCLASSEXW wc = {};
|
|
if (!global_win32_state.process_info.window_class_initialized) {
|
|
global_win32_state.process_info.window_class_initialized = true;
|
|
|
|
wc.cbSize = sizeof(WNDCLASSEXW);
|
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
if (!wnd_proc_override) {
|
|
wc.lpfnWndProc = win32_wnd_proc;
|
|
} else {
|
|
wc.lpfnWndProc = (WNDPROC)wnd_proc_override;
|
|
}
|
|
wc.cbClsExtra = 0;
|
|
wc.cbWndExtra = 0;
|
|
wc.hInstance = nullptr;
|
|
wc.hIcon = get_main_window().icon;
|
|
wc.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW);
|
|
wc.hbrBackground = nullptr;
|
|
wc.lpszMenuName = nullptr;
|
|
wc.lpszClassName = (LPCWSTR)utf8_to_wide(class_name).data;
|
|
|
|
if (RegisterClassExW(&wc) == 0) {
|
|
log_error("RegisterClassExW Failed.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Window_Dimensions wd = { 100, 100, 640, 480 };
|
|
|
|
if (center_window) {
|
|
wd = platform_get_centered_window_dimensions(open_on_largest_monitor);
|
|
log("[Window_Dimensions] location: (%d, %d); size: %dx%d px",
|
|
wd.window_x, wd.window_y, wd.window_width, wd.window_height);
|
|
}
|
|
|
|
HWND hwnd = CreateWindowExW(
|
|
WS_EX_APPWINDOW,
|
|
wc.lpszClassName,
|
|
(LPCWSTR)utf8_to_wide(new_window_name).data,
|
|
WS_OVERLAPPEDWINDOW,
|
|
wd.window_x, wd.window_y, wd.window_width, wd.window_height,
|
|
nullptr, nullptr, nullptr, nullptr
|
|
);
|
|
|
|
if (!hwnd) {
|
|
Assert(false);
|
|
DestroyWindow(hwnd);
|
|
return false;
|
|
}
|
|
|
|
// Display the window:
|
|
if (display_window) {
|
|
UpdateWindow(hwnd);
|
|
ShowWindow(hwnd, SW_SHOW);
|
|
}
|
|
|
|
// Save window to stack
|
|
Window_Info info = {};
|
|
info.window = hwnd;
|
|
info.initial_dimensions = wd;
|
|
info.is_main_window = (parent == nullptr);
|
|
|
|
// Should we mutex this? Seems very unlikely we'll ever need to call this
|
|
// from multiple threads at the same time.
|
|
push_allocator_label("global_win32_state.process_info.windows");
|
|
array_add(global_win32_state.process_info.windows, info);
|
|
|
|
return true;
|
|
}
|
|
|
|
Window_Info get_main_window () {
|
|
Array<Window_Info> windows = global_win32_state.process_info.windows;
|
|
if (windows.count <= 0) {
|
|
return {};
|
|
}
|
|
|
|
for (s64 i = 0; i < windows.count; i += 1) {
|
|
if (windows[i].is_main_window) {
|
|
return windows[i];
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
Window_Info* get_main_window_pointer () {
|
|
s64 window_count = global_win32_state.process_info.windows.count;
|
|
|
|
for (s64 i = 0; i < window_count; i += 1) {
|
|
if (global_win32_state.process_info.windows[i].is_main_window) {
|
|
return &global_win32_state.process_info.windows[i];
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Window_Info* get_window_info (Window_Type window) {
|
|
s64 window_count = global_win32_state.process_info.windows.count;
|
|
|
|
for (s64 i = 0; i < window_count; i += 1) {
|
|
if (global_win32_state.process_info.windows[i].window == window) {
|
|
return &global_win32_state.process_info.windows[i];
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool get_window_dimensions(Window_Info* info, s32* width, s32* height) {
|
|
if (info == nullptr || width == nullptr || height == nullptr) return false;
|
|
Assert(width && height);
|
|
|
|
RECT c = {};
|
|
bool success = GetClientRect(info->window, &c);
|
|
|
|
if (!success) {
|
|
(*width) = 0;
|
|
(*height) = 0;
|
|
return false;
|
|
}
|
|
|
|
(*width) = (s32)(c.right - c.left);
|
|
(*height) = (s32)(c.bottom - c.top);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool os_window_is_minimized (Window_Type window) {
|
|
return (bool)IsIconic(window);
|
|
}
|
|
|
|
bool os_main_window_is_minimized () {
|
|
return os_window_is_minimized(get_main_window().window);
|
|
}
|
|
|
|
s32 os_cpu_logical_core_count () {
|
|
OS_System_Info* info = &global_win32_state.system_info;
|
|
return info->logical_processor_count;
|
|
}
|
|
|
|
s32 os_cpu_physical_core_count () {
|
|
OS_System_Info* info = &global_win32_state.system_info;
|
|
return info->physical_core_count;
|
|
}
|
|
|
|
s32 os_cpu_primary_core_count () {
|
|
OS_System_Info* info = &global_win32_state.system_info;
|
|
return info->primary_core_count;
|
|
}
|
|
|
|
s32 os_cpu_secondary_core_count () {
|
|
OS_System_Info* info = &global_win32_state.system_info;
|
|
return info->secondary_core_count;
|
|
}
|
|
|
|
// #ProgramData
|
|
string os_program_data_path () {
|
|
ArrayView<u16> path = ArrayView<u16>(temp(), Win32_Max_Path_Length + 1);
|
|
HRESULT r = SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, (LPWSTR)path.data);
|
|
if (r != S_OK) {
|
|
log_error("[os_program_data_path] Failed to get program data path.");
|
|
os_log_error();
|
|
}
|
|
|
|
return wide_to_utf8(path.data);
|
|
}
|
|
|
|
// #Drives
|
|
internal Win32_Drive* copy_win32_drive (Win32_Drive* drive) {
|
|
Win32_Drive* result = New<Win32_Drive>();
|
|
|
|
result->label = copy_string(drive->label);
|
|
result->volume_name = copy_string(drive->volume_name);
|
|
result->type = drive->type;
|
|
result->file_system = drive->file_system;
|
|
result->full_size = drive->full_size;
|
|
result->free_space = drive->free_space;
|
|
result->serial_number = drive->serial_number;
|
|
result->max_component_length = drive->max_component_length;
|
|
result->file_system_flags = drive->file_system_flags;
|
|
result->is_present = drive->is_present;
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Win32_Discover_Drives () {
|
|
push_allocator_label("Win32_Discover_Drives");
|
|
global_win32_state.system_info.drive_arena = bootstrap_arena(Arena_Reserve::Size_2M, "global_win32_state.system_info.drive_arena");
|
|
push_arena(global_win32_state.system_info.drive_arena);
|
|
// Initialize drive_table if necessary.
|
|
Table<string, OS_Drive*>* drive_table = get_drive_table();
|
|
if (!drive_table->allocated) {
|
|
drive_table->allocator = default_allocator();
|
|
// #TODO(Low priority): #hash_table need a macro for initializing with string keys!
|
|
drive_table->hash_function = string_hash_function_fnv1a;
|
|
drive_table->compare_function = string_keys_match;
|
|
s64 slots_to_allocate = 64;
|
|
table_init(drive_table, slots_to_allocate);
|
|
}
|
|
|
|
u16 lpBuf[1024];
|
|
u32 result_length = GetLogicalDriveStringsW(1024, (LPWSTR)lpBuf);
|
|
if (!result_length) {
|
|
log_error("GetLogicalDriveStringsW failed!");
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
bool completed = false;
|
|
s32 current_index = 0;
|
|
|
|
while (true) {
|
|
completed = completed || (current_index >= 1024) || lpBuf[current_index] == 0;
|
|
if (completed) break;
|
|
|
|
u16* logical_drive = lpBuf + current_index;
|
|
|
|
string drive_label = wide_to_utf8(logical_drive);
|
|
|
|
u64 total_number_of_bytes;u64 total_number_of_free_bytes;
|
|
bool result = GetDiskFreeSpaceExW((LPCWSTR)logical_drive, nullptr,
|
|
(PULARGE_INTEGER)&total_number_of_bytes, (PULARGE_INTEGER)&total_number_of_free_bytes);
|
|
|
|
Win32_Drive_Type drive_type = (Win32_Drive_Type)GetDriveTypeW((LPCWSTR)logical_drive);
|
|
|
|
log("Found %s drive, type %s", drive_label.data, to_string(drive_type).data);
|
|
|
|
current_index += (s32)(drive_label.count + 1);
|
|
|
|
bool just_added = false;
|
|
OS_Drive** drive_ptr = table_find_or_add(drive_table, drive_label, &just_added);
|
|
OS_Drive* drive = New<OS_Drive>();
|
|
(*drive_ptr) = drive; // fill table slot with data.
|
|
|
|
if (!just_added) { // delete old strings before updating
|
|
// This is silly, but there's a small chance the volume has been renamed so...
|
|
string_free(drive->label); // this is actually just stupid.
|
|
string_free(drive->volume_name);
|
|
}
|
|
|
|
u16 volume_name[Win32_Max_Path_Length] = {};
|
|
u16 file_system_name[Win32_Max_Path_Length] = {};
|
|
DWORD serial_number = 0; DWORD max_comp_len = 0; DWORD file_system_flags = 0;
|
|
if (GetVolumeInformationW((LPCWSTR)logical_drive, (LPWSTR)volume_name,
|
|
Win32_Max_Path_Length, &serial_number, &max_comp_len, &file_system_flags,
|
|
(LPWSTR)file_system_name, Win32_Max_Path_Length)) {
|
|
drive->label = drive_label;
|
|
if (volume_name[0] == 0) {
|
|
drive->volume_name = copy_string("Local Disk");
|
|
} else {
|
|
drive->volume_name = wide_to_utf8(volume_name);
|
|
}
|
|
if (drive->volume_name == "") { drive->volume_name = copy_string("Local Disk"); } // Probably redundant??
|
|
|
|
drive->type = (Win32_Drive_Type)drive_type;
|
|
{ push_allocator(temp());
|
|
drive->file_system = Win32_filesystem_from_string(wide_to_utf8(file_system_name));
|
|
}
|
|
drive->serial_number = serial_number;
|
|
drive->max_component_length = max_comp_len;
|
|
drive->file_system_flags = file_system_flags;
|
|
drive->is_present = true;
|
|
push_allocator(temp());
|
|
log(" - volume name: %s", drive->volume_name.data);
|
|
log(" - file_system: %s", wide_to_utf8(file_system_name).data);
|
|
} else {
|
|
log_error("GetVolumeInformationW failed! (drive label: %s)", drive_label.data);
|
|
os_log_error();
|
|
drive->is_present = false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Drive label includes `:\`
|
|
bool Win32_Drive_Exists (string drive_label) {
|
|
push_allocator(temp());
|
|
LPCWSTR drive_label_wide = (LPCWSTR)utf8_to_wide(drive_label).data;
|
|
UINT type = GetDriveTypeW(drive_label_wide);
|
|
return (type != DRIVE_UNKNOWN && type != DRIVE_NO_ROOT_DIR);
|
|
// Alternative method:
|
|
// return (bool)GetVolumeInformationW(drive_label_wide, nullptr, 0, nullptr, nullptr, nullptr, nullptr, 0);
|
|
}
|
|
|
|
string Win32_drive_letter (string any_path) {
|
|
// #TODO: remove leading `\\.\` if present, assert if drive letter is invalid.
|
|
// we copy so it is null-terminated, and can be used as %s in format_string.
|
|
return copy_string({1, any_path.data});
|
|
}
|
|
|
|
string os_get_machine_name () {
|
|
constexpr u8 WIN32_MAX_COMPUTER_LENGTH_NAME = 31;
|
|
u16 buffer[WIN32_MAX_COMPUTER_LENGTH_NAME + 1];
|
|
|
|
u32 count = WIN32_MAX_COMPUTER_LENGTH_NAME + 1;
|
|
if (GetComputerNameW((LPWSTR)buffer, (LPDWORD)&count)) {
|
|
return wide_to_utf8(buffer);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
// #TODO: #window_creation #window_manipulation
|
|
// [ ] resize_window
|
|
// [ ] position_window
|
|
// [ ] toggle_fullscreen
|
|
// [ ] get_dimensions
|
|
|
|
// #TODO: #window_interaction (mouse/keyboard)
|
|
// [ ] get_mouse_pointer_position
|
|
// [ ] ... What APIs do I need for Keyboard
|
|
|
|
// #FileSort #FileSearch
|
|
|
|
|
|
// #GlobalHotkeys
|
|
struct Win32_Global_Hotkey {
|
|
s32 hotkey_id;
|
|
u32 modifiers;
|
|
u32 virtual_key_code;
|
|
b32 registered;
|
|
};
|
|
|
|
void win32_unregister_global_hotkey (Win32_Global_Hotkey* gh) {
|
|
if (gh->registered) {
|
|
bool success = (bool)UnregisterHotKey(nullptr, gh->hotkey_id);
|
|
if (!success) {
|
|
log_error("Failed to register global hotkey");
|
|
os_log_error();
|
|
}
|
|
Assert(success);
|
|
}
|
|
|
|
gh->registered = false;
|
|
}
|
|
|
|
void win32_register_global_hotkey (Win32_Global_Hotkey* gh) {
|
|
bool success = (bool)RegisterHotKey(nullptr, gh->hotkey_id, gh->modifiers, gh->virtual_key_code);
|
|
if (!success) {
|
|
log_error("Failed to register global hotkey"); // maybe print gh data?
|
|
os_log_error();
|
|
gh->registered = false;
|
|
}
|
|
Assert(success);
|
|
|
|
gh->registered = true;
|
|
}
|
|
|
|
u32 hex_to_int (u8 c) {
|
|
if (c >= 'a') return (c - 'a' + 0xA);
|
|
if (c >= 'A') return (c - 'A' + 0xA);
|
|
if (c >= '0') return (c - '0');
|
|
return 1;
|
|
}
|
|
|
|
GUID guid_from_string (string str) {
|
|
Assert(str.count == 36);
|
|
GUID id; // #NoInit
|
|
// Expecting format: "00000000-0000-0000-C000-000000000046"
|
|
id.Data1 = (hex_to_int(str[0]) << 28) | (hex_to_int(str[1]) << 24) | (hex_to_int(str[2]) << 20) | (hex_to_int(str[3]) << 16) |
|
|
(hex_to_int(str[4]) << 12) | (hex_to_int(str[5]) << 8) | (hex_to_int(str[6]) << 4) | (hex_to_int(str[7]) << 0);
|
|
Assert(str[8] == '-');
|
|
|
|
id.Data2 = ((hex_to_int(str[9]) << 12) | (hex_to_int(str[10]) << 8) | (hex_to_int(str[11]) << 4) | (hex_to_int(str[12]) << 0));
|
|
Assert(str[13] == '-');
|
|
id.Data3 = ((hex_to_int(str[14]) << 12) | (hex_to_int(str[15]) << 8) | (hex_to_int(str[16]) << 4) | (hex_to_int(str[17]) << 0));
|
|
Assert(str[18] == '-');
|
|
id.Data4[0] = ((hex_to_int(str[19]) << 4) | (hex_to_int(str[20]) << 0));
|
|
Assert(str[23] == '-');
|
|
id.Data4[1] = ((hex_to_int(str[21]) << 4) | (hex_to_int(str[22]) << 0));
|
|
id.Data4[2] = ((hex_to_int(str[24]) << 4) | (hex_to_int(str[25]) << 0));
|
|
id.Data4[3] = ((hex_to_int(str[26]) << 4) | (hex_to_int(str[27]) << 0));
|
|
id.Data4[4] = ((hex_to_int(str[28]) << 4) | (hex_to_int(str[29]) << 0));
|
|
id.Data4[5] = ((hex_to_int(str[30]) << 4) | (hex_to_int(str[31]) << 0));
|
|
id.Data4[6] = ((hex_to_int(str[32]) << 4) | (hex_to_int(str[33]) << 0));
|
|
id.Data4[7] = ((hex_to_int(str[34]) << 4) | (hex_to_int(str[35]) << 0));
|
|
|
|
return id;
|
|
}
|
|
|
|
internal HICON get_hicon_used_for_file (string path, bool big_icon=false) {
|
|
SHFILEINFOW sfi = {};
|
|
|
|
push_allocator(temp());
|
|
wstring wide_path = utf8_to_wide(path);
|
|
bool is_directory = path_is_directory(path);
|
|
|
|
u32 dwFileAttributes = (is_directory) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
|
|
|
|
if (!big_icon) {
|
|
u32* result = (u32*)SHGetFileInfoW((LPCWSTR)wide_path.data, (DWORD)dwFileAttributes, &sfi,
|
|
sizeof(SHFILEINFOW), (u32)(SHGFI_ICON | SHGFI_USEFILEATTRIBUTES | SHGFI_LARGEICON));
|
|
|
|
if (result != 0 && sfi.hIcon) {
|
|
return sfi.hIcon;
|
|
} else {
|
|
log_error("Failed to load hicon for path %s", copy_string(path));
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
if (big_icon) {
|
|
u32* result = (u32*)SHGetFileInfoW((LPCWSTR)wide_path.data, (DWORD)dwFileAttributes, &sfi,
|
|
sizeof(SHFILEINFOW), (u32)(SHGFI_SYSICONINDEX));
|
|
// log("[SHGetFileInfoW] result: 0x%p", result);
|
|
GUID guid = guid_from_string("46EB5926-582E-4017-9FDF-E8998DAA0950");
|
|
HIMAGELIST il = {};
|
|
HRESULT r = SHGetImageList(SHIL_JUMBO, (const IID &)guid, (void**)&il);
|
|
if (FAILED(r) || !il) {
|
|
log_error("[get_hicon_used_for_file big_icons=true] SHGetImageList failed!");
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
HICON hIcon = ImageList_GetIcon(il, sfi.iIcon, 0);
|
|
|
|
((IUnknown*)il)->Release();
|
|
|
|
return hIcon;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
force_inline internal void internal_icon_copy_row (ArrayView<u32> src, ArrayView<u32> dst) {
|
|
Assert(src.count == dst.count);
|
|
memcpy(dst.data, src.data, dst.count * sizeof(u32));
|
|
}
|
|
|
|
internal void flip_icon_vertical_in_place (Icon* icon) {
|
|
push_allocator(temp());
|
|
ArrayView<u32> row_copy = ArrayView<u32>(icon->width);
|
|
|
|
for (s64 i = 0; i < icon->height/2; i += 1) {
|
|
u32* start_ptr = (u32*)icon->bitmap.data;
|
|
u32* end_ptr = start_ptr + (icon->width * icon->height);
|
|
ArrayView<u32> top_row = { icon->width, start_ptr + (i * icon->height) };
|
|
ArrayView<u32> bottom_row = { icon->width, end_ptr - ((i+1) * icon->height) };
|
|
|
|
// copy top row to bottom row
|
|
internal_icon_copy_row(top_row, row_copy);
|
|
internal_icon_copy_row(bottom_row, top_row);
|
|
internal_icon_copy_row(row_copy, bottom_row);
|
|
}
|
|
}
|
|
|
|
force_inline Icon* win32_load_large_icon_internal (string full_path) {
|
|
if (!file_exists(full_path)) return nullptr;
|
|
|
|
HICON hIcon = get_hicon_used_for_file(full_path, true);
|
|
if (!hIcon) {
|
|
log_error("[get_hicon_used_for_file] failed for path: %s", copy_string(full_path).data);
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
ICONINFOEXW icon_info = {};
|
|
icon_info.cbSize = sizeof(ICONINFOEXW);
|
|
bool get_icon_success = (bool)GetIconInfoExW(hIcon, &icon_info);
|
|
if (!get_icon_success) {
|
|
log_error("[GetIconInfoExW] failed for path: %s", copy_string(full_path).data);
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
BITMAPINFO bitmap_info = {};
|
|
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
|
|
HDC hdc = get_main_window_pointer()->hdc;
|
|
|
|
s32 result = GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr,
|
|
&bitmap_info, DIB_RGB_COLORS);
|
|
if (result == 0 || result == ERROR_INVALID_PARAMETER) {
|
|
log_error("[1] GetDIBits failed!");
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
// Force a known-safe format:
|
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
|
bitmap_info.bmiHeader.biBitCount = 32;
|
|
bitmap_info.bmiHeader.biPlanes = 1;
|
|
|
|
Icon* icon = New<Icon>();
|
|
icon->path = copy_string(full_path);
|
|
icon->width = bitmap_info.bmiHeader.biWidth;
|
|
icon->height = bitmap_info.bmiHeader.biHeight;
|
|
icon->bitmap = ArrayView<u8>(bitmap_info.bmiHeader.biSizeImage);
|
|
|
|
result = GetDIBits(hdc, icon_info.hbmColor, 0, bitmap_info.bmiHeader.biHeight, icon->bitmap.data,
|
|
&bitmap_info, DIB_RGB_COLORS);
|
|
if (result == 0 || result == ERROR_INVALID_PARAMETER) {
|
|
log_error("[2] GetDIBits failed!");
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
flip_icon_vertical_in_place(icon);
|
|
|
|
DeleteObject(icon_info.hbmColor);
|
|
DeleteObject(icon_info.hbmMask);
|
|
DestroyIcon(hIcon);
|
|
|
|
return icon;
|
|
}
|
|
|
|
void delete_icon (Icon* icon) {
|
|
string_free(icon->path);
|
|
array_free(icon->bitmap);
|
|
internal_free(icon);
|
|
}
|
|
|
|
Icon* load_large_icon (string full_path) { // @allocates
|
|
return win32_load_large_icon_internal(full_path);
|
|
}
|
|
|
|
bool win32_open_file_unattached_no_args_v2 (string full_path) {
|
|
push_allocator(temp());
|
|
wstring full_path_w = utf8_to_wide(full_path);
|
|
|
|
// 1. Create the job object
|
|
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
|
if (!job) {
|
|
log("CreateJobObject failed: %lu\n", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
// 2. Enable SILENT breakaway
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = {};
|
|
info.BasicLimitInformation.LimitFlags =
|
|
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
|
|
|
|
if (!SetInformationJobObject(
|
|
job,
|
|
JobObjectExtendedLimitInformation,
|
|
&info,
|
|
sizeof(info))) {
|
|
log_error("SetInformationJobObject failed");
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
// 3. Assign *yourself* to the job
|
|
if (!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
|
log_error("AssignProcessToJobObject failed");
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
STARTUPINFOW si = {};
|
|
si.cb = sizeof(si);
|
|
|
|
PROCESS_INFORMATION pi = {};
|
|
|
|
bool success = CreateProcessW((LPCWSTR)full_path_w.data, nullptr,
|
|
nullptr, nullptr,
|
|
FALSE, 0,
|
|
nullptr, nullptr,
|
|
&si, &pi);
|
|
|
|
if (!success) {
|
|
log_error("[win32_open_file_unattached_no_args] CreateProcessW(%s) failed", wide_to_utf8(full_path_w.data).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(job);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool win32_select_item_in_explorer (string path) {
|
|
push_allocator(temp());
|
|
string quoted_path = format_string("/select,\"%s\"", copy_string(path).data);
|
|
HINSTANCE result = ShellExecuteW(nullptr, L"open",
|
|
L"explorer.exe", (LPCWSTR)utf8_to_wide(quoted_path).data,
|
|
nullptr, SW_SHOWNORMAL);
|
|
if ((s64)result <= 32) {
|
|
log_error("[win32_select_item_in_explorer] ShellExecuteW failed with args `open explorer.exe %s`",
|
|
quoted_path);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool win32_open_directory_in_explorer (string path) {
|
|
push_allocator(temp());
|
|
string quoted_path = format_string("\"%s\"", copy_string(path).data);
|
|
HINSTANCE result = ShellExecuteW(nullptr, L"open",
|
|
L"explorer.exe", (LPCWSTR)utf8_to_wide(quoted_path).data,
|
|
nullptr, SW_SHOWNORMAL);
|
|
if ((s64)result <= 32) {
|
|
log_error("[win32_open_directory_in_explorer] ShellExecuteW failed with args `open explorer.exe %s`",
|
|
quoted_path);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool win32_open_in_file_pilot (string path) {
|
|
push_allocator(temp());
|
|
string quoted_path = format_string("\"%s\"", copy_string(path).data);
|
|
HINSTANCE result = ShellExecuteW(nullptr, nullptr,
|
|
L"FPilot.exe", (LPCWSTR)utf8_to_wide(quoted_path).data,
|
|
nullptr, SW_SHOWNORMAL);
|
|
if ((s64)result <= 32) {
|
|
log_error("[win32_open_in_file_pilot] ShellExecuteW failed with args `null FPilot.exe %s`",
|
|
quoted_path);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
force_inline bool win32_select_item_in_file_explorer (string path) {
|
|
OS_Process_Info* info = &global_win32_state.process_info;
|
|
switch (info->open_directories_in) {
|
|
case Open_Directories_In::Explorer: {
|
|
return win32_select_item_in_explorer(path);
|
|
}
|
|
case Open_Directories_In::FPilot: {
|
|
return win32_open_in_file_pilot(path);
|
|
}
|
|
default:
|
|
log_error("[win32_open_directory], global_win32_state.process_info.open_directories_in is not a recognized value: %d", info->open_directories_in);
|
|
Assert(false); // invalid state!
|
|
return false;
|
|
}
|
|
}
|
|
|
|
force_inline bool win32_open_directory (string path) {
|
|
// decide to open in explorer or FPilot:
|
|
OS_Process_Info* info = &global_win32_state.process_info;
|
|
switch (info->open_directories_in) {
|
|
case Open_Directories_In::Explorer: {
|
|
return win32_open_directory_in_explorer(path);
|
|
}
|
|
case Open_Directories_In::FPilot: {
|
|
return win32_open_in_file_pilot(path);
|
|
}
|
|
default:
|
|
log_error("[win32_open_directory], global_win32_state.process_info.open_directories_in is not a recognized value: %d", info->open_directories_in);
|
|
Assert(false); // invalid state!
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool win32_open_file_in_default_program (string path, string working_directory) {
|
|
if (path_is_directory(path)) {
|
|
return win32_open_directory(path);
|
|
}
|
|
|
|
push_allocator(temp());
|
|
|
|
// We use ShellExecuteExW so we can properly detatch from new launched process.
|
|
wstring full_path_w = utf8_to_wide(path);
|
|
wstring working_dir_w = utf8_to_wide(working_directory);
|
|
|
|
SHELLEXECUTEINFOW sei = {};
|
|
sei.cbSize = sizeof(SHELLEXECUTEINFOW);
|
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS; // |SEE_MASK_NO_CONSOLE
|
|
sei.lpFile = (LPCWSTR)full_path_w.data;
|
|
sei.lpDirectory = (LPCWSTR)working_dir_w.data;
|
|
sei.nShow = SW_SHOWNORMAL;
|
|
|
|
bool success = (bool)ShellExecuteExW(&sei);
|
|
if (!success) {
|
|
log_error("[win32_open_file_in_default_program] ShellExecuteExW failed with args path: `%s`, working_directory: `%s`",
|
|
wide_to_utf8(full_path_w).data, wide_to_utf8(working_dir_w).data);
|
|
os_log_error();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool win32_open_file_unattached_no_args (string full_path) {
|
|
/////// VERSION 1 -- DOESN'T DETACH
|
|
// SHELLEXECUTEINFOW sei = {};
|
|
// sei.cbSize = sizeof(SHELLEXECUTEINFOW);
|
|
// sei.fMask = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
|
|
// sei.lpVerb = L"open";
|
|
// // sei.lpVerb = L"runas"; // launch as administrator
|
|
// sei.lpFile = (LPCWSTR)full_path_w.data;
|
|
// sei.nShow = SW_SHOWNORMAL;
|
|
|
|
// if (!ShellExecuteExW(&sei)) {
|
|
// log_error("[win32_open_file_unattached_no_args] ShellExecuteExW(%s) failed", wide_to_utf8(full_path_w.data).data);
|
|
// os_log_error();
|
|
// return false;
|
|
// }
|
|
|
|
/////// VERSION 2 -- TESTING...
|
|
push_allocator(temp());
|
|
wstring full_path_w = utf8_to_wide(full_path);
|
|
|
|
STARTUPINFOW si = {};
|
|
si.cb = sizeof(si);
|
|
|
|
PROCESS_INFORMATION pi = {};
|
|
|
|
bool success = CreateProcessW((LPCWSTR)full_path_w.data, nullptr,
|
|
nullptr, nullptr,
|
|
FALSE, CREATE_BREAKAWAY_FROM_JOB,
|
|
nullptr, nullptr,
|
|
&si, &pi);
|
|
|
|
if (!success) {
|
|
log_error("[win32_open_file_unattached_no_args] CreateProcessW(%s) failed", wide_to_utf8(full_path_w.data).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
// If we launch applications in our job object, they will quit when
|
|
// this application quits, so we have to do this chicanery to get the
|
|
// launched exes to be independent:
|
|
// JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = {};
|
|
// info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_BREAKAWAY_OK;
|
|
|
|
// bool success = (bool)SetInformationJobObject(
|
|
// job,
|
|
// JobObjectExtendedLimitInformation,
|
|
// &info,
|
|
// sizeof(info)
|
|
// );
|
|
// if (!success) {
|
|
// log_error("SetInformationJobObject failed!");
|
|
// os_log_error();
|
|
// }
|
|
// sei.hProcess should get registered?
|
|
|
|
return true;
|
|
}
|
|
|
|
void open_exe_unattached_old (string full_path) {
|
|
// Old version: `Open_Exe_Unattached`
|
|
push_allocator(temp());
|
|
wstring full_path_w = utf8_to_wide(full_path);
|
|
|
|
// Dirty no-good low-down hack to get the new process to remain separate from this process.
|
|
// Everything else I tried did NOT work. This is basically just changing the parent of our
|
|
// job process to another process, so if we exit, it stays alive.
|
|
// In this case explorer is the one that launches the program.
|
|
// This is also WAY slower than just calling CreateProcessW
|
|
bool success = (bool)ShellExecuteW(nullptr, L"open", L"explorer.exe",
|
|
(LPCWSTR)full_path_w.data, nullptr, SW_SHOWNORMAL);
|
|
if (!success) {
|
|
log_error("ShellExecuteW(open explorer.exe %s) failed", wide_to_utf8(full_path_w).data);
|
|
os_log_error();
|
|
}
|
|
}
|
|
|
|
force_inline Icon* win32_load_small_icon_internal (string full_path) {
|
|
if (!file_exists(full_path)) return nullptr;
|
|
|
|
HICON hIcon = get_hicon_used_for_file(full_path, false);
|
|
if (!hIcon) {
|
|
log_error("[get_hicon_used_for_file] failed for path: %s", copy_string(full_path).data);
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
ICONINFOEXW icon_info = {};
|
|
icon_info.cbSize = sizeof(ICONINFOEXW);
|
|
bool get_icon_success = (bool)GetIconInfoExW(hIcon, &icon_info);
|
|
if (!get_icon_success) {
|
|
log_error("[GetIconInfoExW] failed for path: %s", copy_string(full_path).data);
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
BITMAPINFO bitmap_info = {};
|
|
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
|
|
HDC hdc = get_main_window_pointer()->hdc;
|
|
|
|
s32 result = GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr,
|
|
&bitmap_info, DIB_RGB_COLORS);
|
|
if (result == 0 || result == ERROR_INVALID_PARAMETER) {
|
|
log_error("[win32_load_small_icon_internal][1] GetDIBits failed!");
|
|
os_log_error();
|
|
return nullptr;
|
|
}
|
|
|
|
// Force a known-safe format:
|
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
|
bitmap_info.bmiHeader.biBitCount = 32;
|
|
bitmap_info.bmiHeader.biPlanes = 1;
|
|
|
|
Icon* icon = New<Icon>();
|
|
icon->path = copy_string(full_path);
|
|
icon->width = bitmap_info.bmiHeader.biWidth;
|
|
icon->height = bitmap_info.bmiHeader.biHeight;
|
|
icon->bitmap = ArrayView<u8>(bitmap_info.bmiHeader.biSizeImage);
|
|
|
|
result = GetDIBits(hdc, icon_info.hbmColor, 0, bitmap_info.bmiHeader.biHeight, icon->bitmap.data,
|
|
&bitmap_info, DIB_RGB_COLORS);
|
|
if (result == 0 || result == ERROR_INVALID_PARAMETER) {
|
|
log_error("[2] GetDIBits failed!");
|
|
os_log_error();
|
|
DeleteObject(icon_info.hbmColor);
|
|
DeleteObject(icon_info.hbmMask);
|
|
return nullptr;
|
|
}
|
|
|
|
flip_icon_vertical_in_place(icon);
|
|
|
|
DeleteObject(icon_info.hbmColor);
|
|
DeleteObject(icon_info.hbmMask);
|
|
DestroyIcon(hIcon);
|
|
|
|
return icon;
|
|
}
|
|
|
|
void os_clipboard_set_text (string s) {
|
|
push_allocator(temp());
|
|
|
|
Window_Info* info = get_main_window_pointer();
|
|
|
|
if (!OpenClipboard(info->window)) return;
|
|
|
|
EmptyClipboard();
|
|
|
|
wstring ws = utf8_to_wide(s);
|
|
s64 ws_bytes = ws.count * 2 + 2;
|
|
|
|
HGLOBAL clipbuffer = GlobalAlloc(0, (u64)ws_bytes);
|
|
u8* buffer = (u8*)GlobalLock(clipbuffer);
|
|
memcpy(buffer, ws.data, ws_bytes);
|
|
GlobalUnlock(clipbuffer);
|
|
|
|
SetClipboardData(CF_UNICODETEXT, clipbuffer);
|
|
|
|
CloseClipboard();
|
|
}
|
|
|
|
string os_clipboard_get_text () { // #allocates
|
|
Window_Info* info = get_main_window_pointer();
|
|
|
|
if (OpenClipboard(info->window)) {
|
|
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
|
u16* buffer = (u16*)GlobalLock(hData);
|
|
string s = wide_to_utf8(buffer);
|
|
GlobalUnlock(hData);
|
|
CloseClipboard();
|
|
return s;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
void os_clipboard_clear () {
|
|
Window_Info* info = get_main_window_pointer();
|
|
|
|
if (OpenClipboard(info->window)) {
|
|
EmptyClipboard();
|
|
CloseClipboard();
|
|
}
|
|
}
|
|
|
|
bool os_execute_program_with_arguments (string exe_path, string args) {
|
|
push_allocator(temp());
|
|
bool success = (bool)ShellExecuteW(
|
|
nullptr,
|
|
L"open",
|
|
(LPCWSTR)utf8_to_wide(exe_path).data,
|
|
(LPCWSTR)utf8_to_wide(args).data,
|
|
nullptr,
|
|
SW_SHOWNORMAL);
|
|
|
|
if (!success) {
|
|
log_error("[os_execute_program_with_arguments] ShellExecuteW failed with arguments: open `%s %s`",
|
|
copy_string(temp(), exe_path), copy_string(temp(), args));
|
|
os_log_error();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
string powershell_path () {
|
|
push_allocator(temp());
|
|
|
|
u16 sysDir[Win32_Max_Path_Length];
|
|
u32 chars_copied = GetSystemDirectoryW((LPWSTR)sysDir, Win32_Max_Path_Length);
|
|
if (chars_copied == 0) {
|
|
log_error("[GetSystemDirectoryW] failed!");
|
|
os_log_error();
|
|
return "";
|
|
}
|
|
|
|
wcscat((LPWSTR)sysDir, L"\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
// Any better way to get this?
|
|
// "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
|
return copy_string(temp(), trim(wide_to_utf8(sysDir)));
|
|
}
|
|
|
|
bool win32_run_powershell_script (string script) {
|
|
// calls os_execute_program_with_arguments
|
|
bool success = os_execute_program_with_arguments(powershell_path(), script);
|
|
if (!success) {
|
|
log_error("[win32_run_powershell_script] failed to execute with script: `%s`",
|
|
copy_string(temp(), script).data);
|
|
os_log_error();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool os_run_shell_script (string script_path, string working_directory) {
|
|
// should have some global setting for preferred terminal to open
|
|
// For now, default to powershell.
|
|
push_allocator(temp());
|
|
string powershell_script = format_string("-NoExit -Command \"Set-Location '%s'\"; & '%s'",
|
|
copy_string(working_directory).data, copy_string(script_path).data);
|
|
|
|
return win32_run_powershell_script(powershell_script);
|
|
}
|
|
|
|
bool os_safe_delete_file (string path) {
|
|
push_allocator(temp());
|
|
u16 from[Win32_Max_Path_Length + 2] = {};
|
|
// #TODO: must be double null terminated:
|
|
wstring path_w = utf8_to_wide(path);
|
|
Assert(path_w.count < Win32_Max_Path_Length);
|
|
memcpy(from, path_w.data, path_w.count * sizeof(u16));
|
|
|
|
SHFILEOPSTRUCTW op = {};
|
|
op.wFunc = FO_DELETE;
|
|
op.pFrom = (PCZZWSTR)from;
|
|
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
|
|
|
|
// SHFileOperation fails on any path prefixed with "\?".
|
|
return SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted;
|
|
}
|
|
|
|
bool os_delete_directory (string path) {
|
|
push_allocator(temp());
|
|
auto_release_temp();
|
|
|
|
wstring path_w = utf8_to_wide(path);
|
|
|
|
u32 full_path_size = GetFullPathNameW((LPCWSTR)path_w.data, 0, nullptr, nullptr);
|
|
if (!full_path_size) {
|
|
log_error("[os_delete_directory] GetFullPathNameW failed with input `%s`, 0, null, null",
|
|
copy_string(path).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
wstring absolute_path_wide = wstring((full_path_size + 1));
|
|
u32 final_length = GetFullPathNameW((LPCWSTR)path_w.data, full_path_size, (LPWSTR)absolute_path_wide.data, nullptr);
|
|
if (!final_length) {
|
|
log_error("[os_delete_directory] GetFullPathNameW failed with input `%s`, `%d`, `absolute_path_wide`, null",
|
|
copy_string(path).data, full_path_size);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
Assert(final_length <= full_path_size-1); // -1 because on success, it does NOT include the null character, but on failure it does. This used to be ==, but in some cases we get a shorter path... I guess the return value of GetFullPathNameW is inexact in some cases!
|
|
absolute_path_wide[final_length + 1] = (u16)0; // extra null character after the string-terminating null character.
|
|
|
|
SHFILEOPSTRUCTW op = {};
|
|
op.wFunc = FO_DELETE;
|
|
op.pFrom = (PCZZWSTR)absolute_path_wide.data;
|
|
// op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
|
|
|
|
s32 result = SHFileOperationW(&op);
|
|
if (result != 0) {
|
|
log_error("[os_delete_directory] SHFileOperationW failed! See error code:");
|
|
os_log_error();
|
|
return false;
|
|
} else if (op.fAnyOperationsAborted) {
|
|
log_error("[os_delete_directory] SHFileOperationW was not able to delete anything (op.fAnyOperationsAborted = true).");
|
|
os_log_error();
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool os_delete_file_or_directory (string path) {
|
|
if (path_is_directory(path)) {
|
|
return os_delete_directory(path);
|
|
}
|
|
|
|
push_allocator(temp());
|
|
wstring path_w = utf8_to_wide(path);
|
|
return DeleteFileW((LPCWSTR)path_w.data) != 0;
|
|
}
|
|
|
|
void os_clear_unused_pages () {
|
|
SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
|
|
}
|
|
|
|
// #NTFS#MFT
|
|
constexpr u64 MFT_FILE_REFERENCE_ID_ROOT = U64_MAX;
|
|
|
|
struct File_Enumeration_Results {
|
|
Serializer strings; // Serializer?
|
|
ArenaArray<u32> offsets;
|
|
ArenaArray<s16> lengths;
|
|
ArenaArray<u64> sizes;
|
|
ArenaArray<u64> modtimes;
|
|
// Index of the parent directory
|
|
ArenaArray<u64> parent_indices;
|
|
|
|
// For back-linking (remove and put into NTFS_mft_read_raw) (Temporary)
|
|
ArenaArray<u64> reference_ids; // FRN
|
|
ArenaArray<u64> parent_ids; // Link to parent FRN.
|
|
};
|
|
|
|
|
|
s64 array_bytes (File_Enumeration_Results* results) {
|
|
if (!results) return 0;
|
|
s64 total_bytes = 0;
|
|
total_bytes += (results->offsets.count * sizeof(u32));
|
|
total_bytes += (results->lengths.count * sizeof(s16));
|
|
total_bytes += (results->sizes.count * sizeof(u64));
|
|
total_bytes += (results->modtimes.count * sizeof(u64));
|
|
total_bytes += (results->parent_indices.count * sizeof(u64));
|
|
|
|
total_bytes += (results->reference_ids.count * sizeof(u64));
|
|
total_bytes += (results->parent_ids.count * sizeof(u64));
|
|
|
|
return total_bytes;
|
|
}
|
|
|
|
void file_enumeration_results_init (File_Enumeration_Results* results) {
|
|
arena_array_init(&results->strings, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: strings");
|
|
arena_array_init(&results->offsets, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: offsets");
|
|
arena_array_init(&results->lengths, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: lengths");
|
|
arena_array_init(&results->sizes, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: sizes");
|
|
arena_array_init(&results->modtimes, Arena_Reserve::Size_2G, 0,"File_Enumeration_Results: modtimes");
|
|
|
|
arena_array_init(&results->reference_ids, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: reference_ids");
|
|
arena_array_init(&results->parent_ids, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: parent_ids");
|
|
|
|
arena_array_init(&results->parent_indices, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: parent_indices");
|
|
}
|
|
|
|
void file_enumeration_results_free (File_Enumeration_Results* results) {
|
|
arena_array_free(results->strings);
|
|
arena_array_free(results->offsets);
|
|
arena_array_free(results->lengths);
|
|
arena_array_free(results->sizes);
|
|
arena_array_free(results->modtimes);
|
|
|
|
// arena_array_free(results->reference_ids);
|
|
// arena_array_free(results->parent_ids);
|
|
|
|
arena_array_free(results->parent_indices);
|
|
}
|
|
|
|
struct NTFS_MFT_Enumeration_Info {
|
|
bool thread_started;
|
|
f64 start_time;
|
|
f64 end_time;
|
|
};
|
|
|
|
struct NTFS_MFT_Data {
|
|
Win32_Drive* drive; // copy! so we can verify data!
|
|
// data: see Win32_File_Enumeration_Drive, minus the bit tables
|
|
File_Enumeration_Results dirs;
|
|
File_Enumeration_Results files;
|
|
|
|
// #SortIndices (RadixSort, Qsort_r)
|
|
Arena* arena; // for backing below indices
|
|
// #TODO: allocate RadixSort with temp(), then array_copy the results with arena.
|
|
ArrayView<u32> dirs_sorted_modtime;
|
|
ArrayView<u32> files_sorted_modtime;
|
|
ArrayView<u32> dirs_sorted_size;
|
|
ArrayView<u32> files_sorted_size;
|
|
ArrayView<u32> dirs_sorted_name;
|
|
ArrayView<u32> files_sorted_name;
|
|
ArrayView<u32> dirs_sorted_path;
|
|
ArrayView<u32> files_sorted_path;
|
|
|
|
f64 start_time;
|
|
f64 end_time;
|
|
};
|
|
|
|
struct MFT_File_Info {
|
|
string name;
|
|
u64 parent_index;
|
|
};
|
|
|
|
MFT_File_Info file_info (NTFS_MFT_Data* mft_data, s64 src_index, bool is_directory) {
|
|
File_Enumeration_Results* f =
|
|
(is_directory) ? &mft_data->dirs : &mft_data->files;
|
|
|
|
s64 name_count = (f->lengths)[src_index];
|
|
u32 offset = (f->offsets)[src_index];
|
|
u8* string_ptr = &f->strings.data[offset];
|
|
|
|
MFT_File_Info info = {};
|
|
info.name = {name_count, string_ptr};
|
|
info.parent_index = f->parent_indices[src_index];
|
|
|
|
return info;
|
|
}
|
|
|
|
s64 file_count (NTFS_MFT_Data* mft_data) {
|
|
File_Enumeration_Results* f = &mft_data->files;
|
|
return f->offsets.count;
|
|
}
|
|
|
|
s64 directory_count (NTFS_MFT_Data* mft_data) {
|
|
File_Enumeration_Results* f = &mft_data->dirs;
|
|
return f->offsets.count;
|
|
}
|
|
|
|
string mft_drive_label (NTFS_MFT_Data* mft_data) {
|
|
push_allocator(temp());
|
|
return copy_string(mft_data->drive->label);
|
|
}
|
|
|
|
string full_path (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
|
|
File_Enumeration_Results* f =
|
|
(is_directory) ? &mft_data->dirs : &mft_data->files;
|
|
|
|
Array<string> paths_reverse = {};
|
|
paths_reverse.allocator = temp();
|
|
|
|
// Root
|
|
MFT_File_Info info = file_info(mft_data, index, is_directory);
|
|
string directory_name = info.name; // last item in
|
|
|
|
while (info.parent_index != MFT_FILE_REFERENCE_ID_ROOT && info.parent_index != 0) {
|
|
info = file_info(mft_data, info.parent_index, true);
|
|
array_add(paths_reverse, info.name);
|
|
}
|
|
|
|
String_Builder* sb = context_builder();
|
|
reset_string_builder(sb, true);
|
|
|
|
string drive_label = mft_drive_label(mft_data);
|
|
append(sb, drive_label);
|
|
|
|
for_each_reverse(i, paths_reverse) {
|
|
append(sb, paths_reverse[i]);
|
|
append(sb, "\\");
|
|
}
|
|
|
|
append(sb, directory_name);
|
|
|
|
return builder_to_string(sb);
|
|
}
|
|
|
|
FILETIME file_modtime (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
|
|
File_Enumeration_Results* f =
|
|
(is_directory) ? &mft_data->dirs : &mft_data->files;
|
|
|
|
FILETIME ft;
|
|
memcpy(&ft, &(f->modtimes)[index], sizeof(u64));
|
|
|
|
return ft;
|
|
}
|
|
|
|
s64 file_size_bytes (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
|
|
File_Enumeration_Results* f =
|
|
(is_directory) ? &mft_data->dirs : &mft_data->files;
|
|
|
|
return (s64)(f->sizes[index]);
|
|
}
|
|
|
|
struct NTFS_MFT_Enumeration {
|
|
Arena* arena;
|
|
ArrayView<OS_Drive*> drives;
|
|
ArrayView<NTFS_MFT_Data> results;
|
|
};
|
|
|
|
// Singleton data because we only want one set of threads enumerating disks at one time.
|
|
global Arena* ntfs_mft_setup_arena;
|
|
global Thread* ntfs_mft_master_thread; // This thread isn't really necessary, it's just used so we can get the most accurate timing.
|
|
global Thread_Group* ntfs_mft_thread_group;
|
|
|
|
global NTFS_MFT_Enumeration ntfs_mft_data;
|
|
global NTFS_MFT_Enumeration_Info ntfs_mft_enum_info;
|
|
|
|
// force_inline // should this be inlined??
|
|
void NTFS_MFT_add_record (NTFS_MFT_Data* mft_data, NTFS_File* file) {
|
|
wstring file_name_w = {file->name_count, file->name_data};
|
|
string file_name = wide_to_utf8(file_name_w);
|
|
File_Enumeration_Results* r;
|
|
if (file->is_directory) {
|
|
r = &mft_data->dirs;
|
|
} else {
|
|
r = &mft_data->files;
|
|
}
|
|
|
|
u32 offset = AddString_NoCount(&r->strings, file_name.data, (s16)file_name.count);
|
|
array_add((r->offsets), offset);
|
|
array_add((r->lengths), (s16)file_name.count);
|
|
// Use the existing file reference ids to link files and parent directories:
|
|
array_add((r->modtimes), file->file_modtime);
|
|
array_add((r->sizes), file->file_size);
|
|
|
|
array_add((r->reference_ids), file->record_id);
|
|
array_add((r->parent_ids), file->parent_id);
|
|
}
|
|
|
|
// Replace add_record and follow pattern in `win32_file_enum_thread_proc`
|
|
bool NTFS_read_internal (NTFS_MFT_Internal* mft, void* buffer, u64 from, u64 count) {
|
|
s32 high = (s32)(from >> 32);
|
|
DWORD result = SetFilePointer(mft->handle, (s32)(from & 0xFFFFFFFF), (PLONG)&high, FILE_BEGIN);
|
|
if (result == INVALID_SET_FILE_POINTER) {
|
|
log_error("SetFilePointer to %p on drive %s failed!", from, copy_string(mft->drive_label).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
u32 bytes_accessed_internal;
|
|
BOOL success = ReadFile(mft->handle, buffer, (DWORD)count, (LPDWORD)&bytes_accessed_internal, nullptr);
|
|
if (!success) {
|
|
log_error("ReadFile @ %p on drive %s failed!", from, copy_string(mft->drive_label).data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
mft->bytes_accessed += bytes_accessed_internal;
|
|
|
|
return bytes_accessed_internal == count;
|
|
}
|
|
|
|
bool NTFS_MFT_read_raw (NTFS_MFT_Data* mft_data) {
|
|
Assert(mft_data != nullptr);
|
|
if (mft_data == nullptr) { return false; }
|
|
|
|
auto_release_temp();
|
|
push_allocator(temp());
|
|
|
|
string drive_path = copy_string(mft_data->drive->label);
|
|
if (mft_data->drive->file_system != File_System::NTFS) {
|
|
log_warning("[NTFS_MFT_read_raw] Failed to enumerate %s as it is not an NTFS drive!", drive_path.data);
|
|
}
|
|
|
|
string drive_letter = Win32_drive_letter(drive_path); // copies.
|
|
|
|
string create_file_target = format_string("\\\\.\\%s:", drive_letter.data);
|
|
HANDLE file_handle = CreateFileA((LPCSTR)create_file_target.data, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
|
|
if (file_handle == INVALID_HANDLE_VALUE) {
|
|
log_error("CreateFileA failed on target %s. Most likely you do not have admin permissions.", create_file_target.data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
NTFS_MFT_Internal* mft = new_ntfs_mft_internal();
|
|
mft->handle = file_handle;
|
|
mft->drive_label = drive_path;
|
|
|
|
bool success;
|
|
NTFS_BootSector boot_sector;
|
|
success = NTFS_read_internal(mft, &boot_sector, 0, 512);
|
|
if (!success) {
|
|
log_error("[NTFS_MFT_read_raw] Failed to read boot sector for drive %s!", drive_path.data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
u64 bytes_per_cluster = (boot_sector.bytesPerSector * boot_sector.sectorsPerCluster);
|
|
success = NTFS_read_internal(mft, mft->mft_file.data, boot_sector.mftStart * bytes_per_cluster, NTFS_MFT_File_Record_Size);
|
|
if (!success) {
|
|
log_error("[NTFS_MFT_read_raw] Failed to read MFT for drive %s! This drive may be corrupted.", drive_path.data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
|
|
NTFS_FileRecordHeader* file_record_start = (NTFS_FileRecordHeader*)mft->mft_file.data;
|
|
if (file_record_start->magic != 0x454C4946) {
|
|
log_error("[NTFS_read_drive_raw] Magic number check failed! This drive is not NTFS or is corrupted!");
|
|
return false;
|
|
}
|
|
|
|
NTFS_AttributeHeader* attribute = (NTFS_AttributeHeader*)(mft->mft_file.data + file_record_start->firstAttributeOffset);
|
|
NTFS_NonResidentAttributeHeader* data_attribute = nullptr;
|
|
u64 approximate_record_count = 0;
|
|
|
|
while (true) {
|
|
if (attribute->attributeType == 0x80) {
|
|
data_attribute = (NTFS_NonResidentAttributeHeader*)attribute;
|
|
} else if (attribute->attributeType == 0xB0) {
|
|
approximate_record_count = ((NTFS_NonResidentAttributeHeader*)attribute)->attributeSize * 8;
|
|
} else if (attribute->attributeType == 0xFFFFFFFF) {
|
|
break;
|
|
}
|
|
|
|
attribute = (NTFS_AttributeHeader*) ((u8*) attribute + attribute->length);
|
|
} // while (true)
|
|
|
|
Assert(data_attribute != nullptr);
|
|
|
|
NTFS_RunHeader* dataRun = (NTFS_RunHeader*)((u8*)data_attribute + data_attribute->dataRunsOffset);
|
|
u64 cluster_number = 0, records_processed = 0;
|
|
|
|
// outer loop
|
|
while (((u8*)dataRun - (u8*)data_attribute) < data_attribute->length && dataRun->lengthFieldBytes) {
|
|
u64 length = 0, offset = 0;
|
|
|
|
for (u8 i = 0; i < dataRun->lengthFieldBytes; i += 1) {
|
|
length |= (u64)(((u8*)dataRun)[1 + i]) << (i * 8);
|
|
}
|
|
|
|
for (u8 i = 0; i < dataRun->offsetFieldBytes; i += 1) {
|
|
offset |= (u64)(((u8*)dataRun)[1 + dataRun->lengthFieldBytes + i]) << (i * 8);
|
|
}
|
|
|
|
if (offset & ((u64) 1 << (dataRun->offsetFieldBytes * 8 - 1))) {
|
|
for (s64 i = dataRun->offsetFieldBytes; i < 8; i += 1) {
|
|
offset |= ((u64)0xFF << (u64)(i * 8));
|
|
}
|
|
}
|
|
|
|
cluster_number += offset;
|
|
dataRun = (NTFS_RunHeader*)((u8*)dataRun + 1 + dataRun->lengthFieldBytes + dataRun->offsetFieldBytes);
|
|
|
|
u64 files_remaining = length * bytes_per_cluster / NTFS_MFT_File_Record_Size;
|
|
u64 position_in_block = 0;
|
|
|
|
while (files_remaining) { // enumerate files in chunks of 65536
|
|
u64 files_to_load = NTFS_MFT_Files_Per_Buffer;
|
|
if (files_remaining < NTFS_MFT_Files_Per_Buffer) {
|
|
files_to_load = files_remaining;
|
|
}
|
|
|
|
success = NTFS_read_internal(mft, mft->mft_buffer.data, cluster_number * bytes_per_cluster + position_in_block, files_to_load * NTFS_MFT_File_Record_Size);
|
|
if (!success) {
|
|
log_error("[NTFS_MFT_read_raw] Failed to read MFT for drive %s! This drive may be corrupted.", drive_path.data);
|
|
os_log_error();
|
|
return false;
|
|
}
|
|
position_in_block += files_to_load * NTFS_MFT_File_Record_Size;
|
|
files_remaining -= files_to_load;
|
|
|
|
for (s64 i = 0; i < (s64)files_to_load; i += 1) { // load
|
|
// Even on an M.2 SSD, processing the file records takes only a fraction of the time to read the data, so there's not much point in multithreading this:
|
|
NTFS_FileRecordHeader* fileRecord = (NTFS_FileRecordHeader*)(mft->mft_buffer.data + NTFS_MFT_File_Record_Size * i);
|
|
records_processed += 1;
|
|
u64 record_id = records_processed - 1;
|
|
|
|
NTFS_File file = {};
|
|
bool file_is_directory = (bool)fileRecord->isDirectory;
|
|
|
|
// A file record may be blank or unused; just skip it.
|
|
if (!fileRecord->inUse) continue;
|
|
|
|
NTFS_AttributeHeader* attribute = (NTFS_AttributeHeader*)((u8*)fileRecord + fileRecord->firstAttributeOffset);
|
|
Assert(fileRecord->magic == 0x454C4946);
|
|
if (fileRecord->magic != 0x454C4946) {
|
|
log_error("[NTFS_read_drive_raw] Magic number check failed! This drive is likely corrupted!");
|
|
return false;
|
|
}
|
|
|
|
// inner loop
|
|
while ((u8*)attribute - (u8*)fileRecord < NTFS_MFT_File_Record_Size) {
|
|
bool is_named = (attribute->nameOffset != 0);
|
|
bool is_nonresident = false;
|
|
// #NOTE: The attribute list is of variable length and terminated with 0xFFFFFFFF. For 1K MFT records, the attribute list starts at offset 0x30.
|
|
// source: https://flatcap.github.io/linux-ntfs/ntfs/concepts/file_record.html
|
|
// 0x10: $STANDARD_INFORMATION - contains file altered time, etc.
|
|
if (attribute->attributeType == 0x10) { // $STANDARD_INFORMATION
|
|
NTFS_FileStandardInformationHeader* fileInfoAttribute = (NTFS_FileStandardInformationHeader*)attribute;
|
|
|
|
file.file_modtime = fileInfoAttribute->modificationTime;
|
|
// DOS file permissions
|
|
add_file_permissions(&file, fileInfoAttribute->filePermissions);
|
|
}
|
|
// 0x30: $FILE_NAME - stores the name of the file attribute (always resident)
|
|
if (attribute->attributeType == 0x30) { // $FILE_NAME
|
|
// #NOTE [VERY IMPORTANT]: All fields, except the parent directory, are only updated when the filename is changed. Until then, they just become out of date. $STANDARD_INFORMATION Attribute, however, will always be kept up-to-date.
|
|
NTFS_FileNameAttributeHeader* fileNameAttribute = (NTFS_FileNameAttributeHeader*)attribute;
|
|
// #namespace: https://flatcap.github.io/linux-ntfs/ntfs/concepts/filename_namespace.html
|
|
// 2 means dos-friendly, but I'm not even sure we need to check this!
|
|
is_nonresident = fileNameAttribute->nonResident;
|
|
|
|
if (fileNameAttribute->namespaceType != 2 && !is_nonresident) {
|
|
// We need both the sequenceNumber and parentRecordNumber here
|
|
u64 sequence_number = ((u64)fileNameAttribute->sequenceNumber) << (u64)48;
|
|
file.parent_id = sequence_number | (u64)((u64)fileNameAttribute->parentRecordNumber & 0xFFFFFFFFFFULL);
|
|
Assert(record_id == (u64)fileRecord->recordNumber);
|
|
file.record_id = (((u64)fileRecord->sequenceNumber) << 48ULL) | (u64)((u64)record_id & 0xFFFFFFFFFFULL);
|
|
file.name_count = fileNameAttribute->fileNameLength;
|
|
file.name_data = (u16*)fileNameAttribute->fileName;
|
|
file.is_directory = file_is_directory;
|
|
}
|
|
}
|
|
// 0x50 = $SECURITY_DESCRIPTOR source: https://flatcap.github.io/linux-ntfs/ntfs/attributes/security_descriptor.html
|
|
// 0x80: $DATA - This Attribute contains the file's data. A file's size is the size of its unnamed Data Stream.
|
|
// https://flatcap.github.io/linux-ntfs/ntfs/concepts/file.html#data
|
|
// #TODO: should skip if file_name is null.
|
|
if (attribute->attributeType == 0x80 && !is_named) { // $DATA
|
|
// #NOTE: The size of the attribute depends on two things. Does it have a name? Is it resident?
|
|
// #TODO: Check if file is compressed then access compressedSize
|
|
// source: https://flatcap.github.io/linux-ntfs/ntfs/concepts/attribute_header.html#flags
|
|
|
|
if (attribute->nonResident) {
|
|
add_flags(&file, attribute->flags); // compressed, encrypted, sparse
|
|
|
|
NTFS_NonResidentAttributeHeader* nonresident_attribute = (NTFS_NonResidentAttributeHeader*)attribute;
|
|
// dataRunsOffset should be 0x40 if attribute is Non-Resident, No Name
|
|
// Assert(nonresident_attribute->dataRunsOffset == 0x40);
|
|
|
|
// This size should be correct even if compressed.
|
|
// #NOTE: VERY IMPORTANT: The top 16 bits are garbage and should be masked out!
|
|
// they do not have any meaning or encode any useful information!
|
|
// s64 attribute_size = nonresident_attribute->attributeSize & 0xFFFFFFFFFFFFULL;
|
|
file.file_size = nonresident_attribute->streamDataSize & 0xFFFFFFFFFFFFULL;
|
|
if (file.is_directory) { file.file_size = 0; }
|
|
}
|
|
}
|
|
|
|
if (attribute->attributeType == 0xFFFFFFFF) {
|
|
// add_record(drive->data, &file);
|
|
// See Dense_FS drive->data
|
|
mft->object_count += 1;
|
|
NTFS_MFT_add_record(mft_data, &file);
|
|
|
|
break;
|
|
}
|
|
|
|
attribute = (NTFS_AttributeHeader*)((u8*)attribute + attribute->length);
|
|
} // while: inner loop
|
|
} // for i: 0..files_to_load-1
|
|
} // while: files_remaining
|
|
} // while: outer loop
|
|
|
|
CloseHandle(file_handle);
|
|
string timer_label = format_string(temp(), "Hash Table Part %s", copy_string(drive_path).data);
|
|
Timed_Block_Print(timer_label);
|
|
|
|
// The Table is temporary so it can live on the stack:
|
|
// This doesn't really need to be an ArenaTable, since we know the number of slots we need..
|
|
auto table = ArenaTable<u64, s32>(); // mapping FRN to Indices
|
|
table_init(&table, directory_count(mft_data), Arena_Reserve::Size_2G, "Directory ID ArenaTable");
|
|
for (s64 i = 0; i < directory_count(mft_data); i += 1) {
|
|
table_set(&table, mft_data->dirs.reference_ids[i], (s32)i);
|
|
}
|
|
|
|
// Link directories
|
|
s32 parent_failed_dir = 0;
|
|
array_resize(mft_data->dirs.parent_indices, directory_count(mft_data), false);
|
|
for (s64 i = 0; i < directory_count(mft_data); i += 1) {
|
|
u64 parent_id = mft_data->dirs.parent_ids[i];
|
|
s32 result_index = 0;
|
|
bool success = table_find(&table, parent_id, &result_index);
|
|
// Assert(success);
|
|
if (success) {
|
|
mft_data->dirs.parent_indices[i] = result_index;
|
|
} else {
|
|
mft_data->dirs.parent_indices[i] = MFT_FILE_REFERENCE_ID_ROOT;
|
|
parent_failed_dir += 1;
|
|
}
|
|
}
|
|
|
|
// Link files
|
|
s32 parent_failed_file = 0;
|
|
array_resize(mft_data->files.parent_indices, file_count(mft_data), false);
|
|
for (s64 i = 0; i < file_count(mft_data); i += 1) {
|
|
u64 parent_id = mft_data->files.parent_ids[i];
|
|
s32 result_index = 0;
|
|
bool success = table_find(&table, parent_id, &result_index);
|
|
// Assert(success);
|
|
if (success) {
|
|
mft_data->files.parent_indices[i] = result_index;
|
|
} else {
|
|
mft_data->files.parent_indices[i] = MFT_FILE_REFERENCE_ID_ROOT;
|
|
parent_failed_file += 1;
|
|
}
|
|
}
|
|
|
|
log("[%s] Failed to find parent for %lld dirs and %lld files", copy_string(drive_path).data, parent_failed_dir, parent_failed_file);
|
|
|
|
// we should be able to purge both reference_id and parent_id
|
|
arena_array_free(mft_data->dirs.reference_ids);
|
|
arena_array_free(mft_data->dirs.parent_ids);
|
|
arena_array_free(mft_data->dirs.sizes); // unused
|
|
arena_array_free(mft_data->files.reference_ids);
|
|
arena_array_free(mft_data->files.parent_ids);
|
|
table_release(&table);
|
|
|
|
// post-processing
|
|
mft_data->end_time = GetUnixTimestamp();
|
|
f64 elapsed_time = mft_data->end_time - mft_data->start_time;
|
|
log("Finished enumeration for drive %s in %s. Found %s records.", copy_string(drive_path).data, format_time_seconds(elapsed_time).data, format_int_with_commas(records_processed).data);
|
|
|
|
return true;
|
|
}
|
|
|
|
Thread_Continue_Status ntfs_mft_enumeration_thread_group_proc (Thread_Group* group, Thread* thread, void* work) {
|
|
auto mft_data = thread_group_task(NTFS_MFT_Data);
|
|
|
|
mft_data->start_time = GetUnixTimestamp();
|
|
|
|
bool success = NTFS_MFT_read_raw(mft_data);
|
|
// Assert(success);
|
|
|
|
return Thread_Continue_Status::CONTINUE;
|
|
}
|
|
|
|
s64 ntfs_mft_enumerate_drives_thread_proc (Thread* thread) {
|
|
auto task = thread_task(NTFS_MFT_Enumeration);
|
|
|
|
s64 work_added = 0;
|
|
s64 work_completed = 0;
|
|
|
|
for_each(d, task->drives) {
|
|
NTFS_MFT_Data* result = &task->results[d];
|
|
|
|
add_work(ntfs_mft_thread_group, result);
|
|
work_added += 1;
|
|
}
|
|
|
|
while (work_completed < work_added) {
|
|
ArrayView<void*> cw = get_completed_work(ntfs_mft_thread_group);
|
|
|
|
work_completed += cw.count;
|
|
Sleep(1);
|
|
}
|
|
|
|
ntfs_mft_enum_info.end_time = GetUnixTimestamp();
|
|
f64 elapsed_time = ntfs_mft_enum_info.end_time - ntfs_mft_enum_info.start_time;
|
|
log("[ntfs_mft_enumerate_drives_thread_proc] Completed %d tasks in %s.",
|
|
work_completed, format_time_seconds(elapsed_time).data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool ntfs_mft_initialize () {
|
|
Timed_Block_Print("ntfs_mft_initialize");
|
|
// This arena contains temporary stuff that's required only to bootstrap enumeration, so it can be discarded at once
|
|
ntfs_mft_setup_arena = bootstrap_arena(Arena_Reserve::Size_64M, "ntfs_mft_setup_arena"); // can probably be a fixed arena
|
|
push_arena(ntfs_mft_setup_arena);
|
|
ntfs_mft_master_thread = New<Thread>();
|
|
string thread_name = "NTFS MFT Enumeration - Master Thread";
|
|
bool success = thread_init(ntfs_mft_master_thread, ntfs_mft_enumerate_drives_thread_proc, thread_name);
|
|
if (!success) {
|
|
log_error("[ntfs_mft_initialize] Failed to initialize thread!");
|
|
os_log_error(); Assert(false);
|
|
return false;
|
|
}
|
|
|
|
ntfs_mft_thread_group = New<Thread_Group>();
|
|
ntfs_mft_thread_group->allocator = allocator(ntfs_mft_setup_arena);
|
|
|
|
// s32 thread_count = 1;
|
|
s32 thread_count = os_cpu_physical_core_count();
|
|
|
|
thread_group_init(ntfs_mft_thread_group, thread_count, ntfs_mft_enumeration_thread_group_proc, "ntfs_mft_enumeration", true);
|
|
|
|
return true;
|
|
}
|
|
|
|
void ntfs_mft_begin_enumeration () { Timed_Block_Print("ntfs_mft_begin_enumeration");
|
|
ntfs_mft_enum_info.thread_started = true;
|
|
ntfs_mft_enum_info.start_time = GetUnixTimestamp();
|
|
|
|
ntfs_mft_data.arena = bootstrap_arena(Arena_Reserve::Size_64M, "ntfs_mft_data.arena");
|
|
push_arena(ntfs_mft_data.arena);
|
|
ntfs_mft_data.drives = os_get_available_drives(); // copies
|
|
ntfs_mft_data.results = ArrayView<NTFS_MFT_Data>(ntfs_mft_data.drives.count);
|
|
// Make copy of Win32_Drive to result using copy_win32_drive
|
|
for_each(d, ntfs_mft_data.drives) {
|
|
Win32_Drive* drive = ntfs_mft_data.drives[d];
|
|
NTFS_MFT_Data* mft_data = &ntfs_mft_data.results[d];
|
|
|
|
file_enumeration_results_init(&mft_data->dirs);
|
|
file_enumeration_results_init(&mft_data->files);
|
|
|
|
mft_data->drive = copy_win32_drive(drive);
|
|
}
|
|
|
|
thread_start(ntfs_mft_master_thread, &ntfs_mft_data);
|
|
thread_group_start(ntfs_mft_thread_group);
|
|
}
|
|
|
|
bool ntfs_mft_enumeration_started () {
|
|
return ntfs_mft_enum_info.thread_started;
|
|
}
|
|
|
|
bool ntfs_mft_enumeration_is_done () {
|
|
if (ntfs_mft_master_thread == nullptr) return true;
|
|
|
|
if (thread_is_done(ntfs_mft_master_thread)) {
|
|
thread_deinit(ntfs_mft_master_thread, true);
|
|
bool success = thread_group_shutdown(ntfs_mft_thread_group);
|
|
Assert(success);
|
|
arena_delete(ntfs_mft_setup_arena);
|
|
ntfs_mft_master_thread = nullptr;
|
|
ntfs_mft_setup_arena = nullptr;
|
|
ntfs_mft_thread_group = nullptr;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|