static void CALLBACK handle_entry_change( DWORD err_code, // completion code DWORD bytes_transfered, // number of bytes transferred LPOVERLAPPED event_container // I/O information buffer ) { WDM_PMonitorCallbackParam param; WDM_PQueueItem data_to_process; if ( err_code == ERROR_OPERATION_ABORTED ) { // Async operation was canceled. This shouldn't happen. // TODO: // 1. Maybe add a union in the queue for errors? // 2. What's the best action when this happens? WDM_DEBUG("Dir handler closed in the process callback!"); return; } if ( ! bytes_transfered ) { WDM_DEBUG("Buffer overflow?! Changes are bigger than the buffer!"); return; } param = (WDM_PMonitorCallbackParam)event_container->hEvent; data_to_process = wdm_queue_item_new(WDM_QUEUE_ITEM_TYPE_DATA); data_to_process->data = wdm_queue_item_data_new(); WDM_WDEBUG("Change detected in '%s'", param->entry->user_data->dir); data_to_process->data->user_data = param->entry->user_data; // Copy change data to the backup buffer memcpy(data_to_process->data->buffer, param->entry->buffer, bytes_transfered); // Add the backup buffer to the change queue wdm_queue_enqueue(param->monitor->changes, data_to_process); // Resume watching the dir for changes register_monitoring_entry(param->entry); // Tell the processing thread to process the changes if ( WaitForSingleObject(param->monitor->process_event, 0) != WAIT_OBJECT_0 ) { // Check if already signaled SetEvent(param->monitor->process_event); } }
static VALUE combined_watch(BOOL recursively, int argc, VALUE *argv, VALUE self) { WDM_PMonitor monitor; WDM_PEntry entry; int directory_letters_count; VALUE directory, flags, os_encoded_directory; BOOL running; // TODO: Maybe raise a more user-friendly error? rb_need_block(); Data_Get_Struct(self, WDM_Monitor, monitor); EnterCriticalSection(&monitor->lock); running = monitor->running; LeaveCriticalSection(&monitor->lock); if ( running ) { rb_raise(eWDM_MonitorRunningError, "You can't watch new directories while the monitor is running!"); } rb_scan_args(argc, argv, "1*", &directory, &flags); Check_Type(directory, T_STRING); entry = wdm_entry_new(); entry->user_data->watch_childeren = recursively; entry->user_data->callback = rb_block_proc(); entry->user_data->flags = RARRAY_LEN(flags) == 0 ? WDM_MONITOR_FLAGS_DEFAULT : extract_flags_from_rb_array(flags); // WTF Ruby source: The original code (file.c) uses the following macro to make sure that the encoding // of the string is ASCII-compatible, but UTF-16LE (Windows default encoding) is not!!! // // FilePathValue(directory); os_encoded_directory = rb_str_encode_ospath(directory); // RSTRING_LEN can't be used because it would return the count of bytes the string uses in its encoding (like UTF-8). // UTF-8 might use more than one byte for the char, which is not needed for WCHAR strings. // Also, the result of MultiByteToWideChar _includes_ the NULL char at the end, which is not true for RSTRING. // // Example: 'C:\Users\Maher\Desktop\تجربة' with __ENCODING__ == UTF-8 // MultiByteToWideChar => 29 (28-char + null) // RSTRING_LEN => 33 (23-char + 10-bytes for 5 Arabic letters which take 2 bytes each) // directory_letters_count = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(os_encoded_directory), -1, NULL, 0); entry->user_data->dir = ALLOCA_N(WCHAR, directory_letters_count); MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(os_encoded_directory), -1, entry->user_data->dir, directory_letters_count); WDM_WDEBUG("New path to watch: '%s'", entry->user_data->dir); entry->user_data->dir = wdm_utils_full_pathname(entry->user_data->dir); if ( entry->user_data->dir == 0 ) { wdm_entry_free(entry); rb_raise(eWDM_Error, "Can't get the absolute path for the passed directory: '%s'!", RSTRING_PTR(directory)); } if ( ! wdm_utils_unicode_is_directory(entry->user_data->dir) ) { wdm_entry_free(entry); rb_raise(eWDM_InvalidDirectoryError, "No such directory: '%s'!", RSTRING_PTR(directory)); } entry->dir_handle = CreateFileW( entry->user_data->dir, // pointer to the file name FILE_LIST_DIRECTORY, // access (read/write) mode FILE_SHARE_READ // share mode | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, // security descriptor OPEN_EXISTING, // how to create FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes NULL ); if ( entry->dir_handle == INVALID_HANDLE_VALUE ) { wdm_entry_free(entry); rb_raise(eWDM_Error, "Can't watch directory: '%s'!", RSTRING_PTR(directory)); } // Store a reference to the entry instead of an event as the event // won't be used when using callbacks. entry->event_container.hEvent = wdm_monitor_callback_param_new(monitor, entry); wdm_monitor_update_head(monitor, entry); WDM_WDEBUG("Watching directory: '%s'", entry->user_data->dir); return Qnil; }
// TODO: // 1. this function uses a lot of 'alloca' calls, which AFAIK is not recommeneded! Can this be avoided? // 2. all wcscat calls can be done faster with memcpy, but is it worth sacrificing the readability? static VALUE extract_absolute_path_from_notification(const LPWSTR base_dir, const PFILE_NOTIFY_INFORMATION info) { LPWSTR buffer, absolute_filepath; WCHAR file[_MAX_FNAME], ext[_MAX_EXT], filename[WDM_MAX_FILENAME]; DWORD filename_len, absolute_filepath_len; LPSTR multibyte_filepath; int multibyte_filepath_buffer_size; VALUE path; filename_len = info->FileNameLength/sizeof(WCHAR); // The file in the 'info' struct is NOT null-terminated, so add 1 extra char to the allocation buffer = ALLOCA_N(WCHAR, filename_len + 1); memcpy(buffer, info->FileName, info->FileNameLength); // Null-terminate the string buffer[filename_len] = L'\0'; WDM_WDEBUG("change in: '%s'", buffer); absolute_filepath_len = wcslen(base_dir) + filename_len; absolute_filepath = ALLOCA_N(WCHAR, absolute_filepath_len + 1); // 1 for NULL absolute_filepath[0] = L'\0'; wcscat(absolute_filepath, base_dir); wcscat(absolute_filepath, buffer); WDM_WDEBUG("absolute path is: '%s'", absolute_filepath); _wsplitpath(buffer, NULL, NULL, file, ext); // TODO: Extracting the file name from 'buffer' is only needed when watching sub-dirs filename[0] = L'\0'; if ( file[0] != L'\0' ) wcscat(filename, file); if ( ext[0] != L'\0' ) wcscat(filename, ext); WDM_WDEBUG("filename: '%s'", filename); filename_len = wcslen(filename); // The maximum length of an 8.3 filename is twelve, including the dot. if (filename_len <= 12 && wcschr(filename, L'~')) { LPWSTR unicode_absolute_filepath; WCHAR absolute_long_filepath[WDM_MAX_WCHAR_LONG_PATH]; BOOL is_unc_path; is_unc_path = wdm_utils_is_unc_path(absolute_filepath); unicode_absolute_filepath = ALLOCA_N(WCHAR, absolute_filepath_len + (is_unc_path ? 8 : 4) + 1); // 8 for "\\?\UNC\" or 4 for "\\?\", and 1 for \0 unicode_absolute_filepath[0] = L'\0'; wcscat(unicode_absolute_filepath, L"\\\\?\\"); if ( is_unc_path ) { wcscat(unicode_absolute_filepath, L"UNC\\"); wcscat(unicode_absolute_filepath, absolute_filepath + 2); // +2 to skip the begin of a UNC path } else { wcscat(unicode_absolute_filepath, absolute_filepath); } // Convert to the long filename form. Unfortunately, this // does not work for deletions, so it's an imperfect fix. if (GetLongPathNameW(unicode_absolute_filepath, absolute_long_filepath, WDM_MAX_WCHAR_LONG_PATH) != 0) { absolute_filepath = absolute_long_filepath + 4; // Skip first 4 pointers of "\\?\" absolute_filepath_len = wcslen(absolute_filepath); WDM_WDEBUG("Short path converted to long: '%s'", absolute_filepath); } else { WDM_DEBUG("Can't convert short path to long: '%s'", rb_w32_strerror(GetLastError())); } } // The convention in Ruby is to use forward-slashes to seprarate dirs on all platforms. wdm_utils_convert_back_to_forward_slashes(absolute_filepath, absolute_filepath_len + 1); // Convert the path from WCHAR to multibyte CHAR to use it in a ruby string multibyte_filepath_buffer_size = WideCharToMultiByte(CP_UTF8, 0, absolute_filepath, absolute_filepath_len + 1, NULL, 0, NULL, NULL); multibyte_filepath = ALLOCA_N(CHAR, multibyte_filepath_buffer_size); if ( 0 == WideCharToMultiByte(CP_UTF8, 0, absolute_filepath, absolute_filepath_len + 1, multibyte_filepath, multibyte_filepath_buffer_size, NULL, NULL) ) { rb_raise(eWDM_Error, "Failed to add the change file path to the event!"); } WDM_DEBUG("will report change in: '%s'", multibyte_filepath); path = rb_enc_str_new(multibyte_filepath, multibyte_filepath_buffer_size - 1, // -1 because this func takes the chars count, not bytes count wdm_rb_enc_utf8); OBJ_TAINT(path); return path; }