// #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 monitors; // Back with default_allocator // #Drives Arena* drive_arena; Table 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 module_load_paths; Array environment_paths; b32 window_class_initialized; Open_Directories_In open_directories_in = Open_Directories_In::FPilot; Arena* event_arena; Array 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 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 result = ArrayView(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* 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* 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_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 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* 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_get_available_drives_sorted () { Allocator ctx_allocator = context_allocator(); push_allocator(temp()); auto drives = os_get_available_drives(); Array 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 drives_sorted = Array(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(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(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->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 read_entire_file (File file, bool add_null_terminator) { ArrayView 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(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 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 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 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 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(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 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 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 path = ArrayView(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(); 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* 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(); (*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 src, ArrayView 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 row_copy = ArrayView(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 top_row = { icon->width, start_ptr + (i * icon->height) }; ArrayView 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->path = copy_string(full_path); icon->width = bitmap_info.bmiHeader.biWidth; icon->height = bitmap_info.bmiHeader.biHeight; icon->bitmap = ArrayView(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->path = copy_string(full_path); icon->width = bitmap_info.bmiHeader.biWidth; icon->height = bitmap_info.bmiHeader.biHeight; icon->bitmap = ArrayView(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 offsets; ArenaArray lengths; ArenaArray sizes; ArenaArray modtimes; // Index of the parent directory ArenaArray parent_indices; // For back-linking (remove and put into NTFS_mft_read_raw) (Temporary) ArenaArray reference_ids; // FRN ArenaArray 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 dirs_sorted_modtime; ArrayView files_sorted_modtime; ArrayView dirs_sorted_size; ArrayView files_sorted_size; ArrayView dirs_sorted_name; ArrayView files_sorted_name; ArrayView dirs_sorted_path; ArrayView 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 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 drives; ArrayView 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(); // 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 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(); 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(); 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.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; }