Musa-STL-Cpp/lib/OS/OS_Win32.cpp

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;
}