file tracking system revision 2
							parent
							
								
									6e9bcb4e36
								
							
						
					
					
						commit
						426b8d5576
					
				
							
								
								
									
										15
									
								
								4ed_system.h
								
								
								
								
							
							
						
						
									
										15
									
								
								4ed_system.h
								
								
								
								
							|  | @ -39,18 +39,11 @@ uhash_equal(Unique_Hash a, Unique_Hash b){ | |||
| #define Sys_Set_File_List_Sig(name) void name(File_List *file_list, String directory) | ||||
| typedef Sys_Set_File_List_Sig(System_Set_File_List); | ||||
| 
 | ||||
| enum{ | ||||
|     TrackFileFlag_ExistingOrFail = 0x0, | ||||
|     TrackFileFlag_NewOrFail = 0x1, | ||||
|     TrackFileFlag_NewAlways = 0x2, | ||||
|     TrackFileFlag_ExistingOrNew = 0x3, | ||||
| }; | ||||
| #define Sys_Add_Listener_Sig(name) Unique_Hash name(char *filename) | ||||
| typedef Sys_Add_Listener_Sig(System_Track_File); | ||||
| 
 | ||||
| #define Sys_Track_File_Sig(name) Unique_Hash name(char *filename, u32 flags) | ||||
| typedef Sys_Track_File_Sig(System_Track_File); | ||||
| 
 | ||||
| #define Sys_Untrack_File_Sig(name) i32 name(Unique_Hash index) | ||||
| typedef Sys_Untrack_File_Sig(System_Untrack_File); | ||||
| #define Sys_Remove_Listener_Sig(name) i32 name(char *filename) | ||||
| typedef Sys_Remove_Listener_Sig(System_Untrack_File); | ||||
| 
 | ||||
| #define Sys_Get_File_Index_Sig(name) i32 name(char *filename, Unique_Hash *index) | ||||
| typedef Sys_Get_File_Index_Sig(System_Get_File_Index); | ||||
|  |  | |||
|  | @ -23,9 +23,6 @@ enum{ | |||
|     FileTrack_MemoryTooSmall, | ||||
|     FileTrack_OutOfTableMemory, | ||||
|     FileTrack_OutOfListenerMemory, | ||||
|     FileTrack_FileNotFound, | ||||
|     FileTrack_FileAlreadyTracked, | ||||
|     FileTrack_FileNotTracked, | ||||
|     FileTrack_NoMoreEvents, | ||||
|     FileTrack_FileSystemError | ||||
| }; | ||||
|  | @ -34,30 +31,7 @@ typedef struct{ | |||
|     uint8_t opaque[128]; | ||||
| } File_Track_System; | ||||
| 
 | ||||
| typedef struct{ | ||||
|     uint8_t opaque[16]; | ||||
| } File_Temp_Handle; | ||||
| 
 | ||||
| typedef struct{ | ||||
|     uint32_t id[4]; | ||||
| } File_Index; | ||||
| 
 | ||||
| static File_Index | ||||
| zero_file_index(){ | ||||
|     File_Index a = {0}; | ||||
|     return(a); | ||||
| } | ||||
| 
 | ||||
| static int32_t | ||||
| file_index_eq(File_Index a, File_Index b){ | ||||
|     return ((a.id[0] == b.id[0]) && | ||||
|             (a.id[1] == b.id[1]) && | ||||
|             (a.id[2] == b.id[2]) && | ||||
|             (a.id[3] == b.id[3])); | ||||
| } | ||||
| 
 | ||||
| typedef int32_t  File_Track_Result; | ||||
| typedef uint64_t File_Time; | ||||
| 
 | ||||
| File_Track_Result | ||||
| init_track_system(File_Track_System *system, | ||||
|  | @ -65,34 +39,10 @@ init_track_system(File_Track_System *system, | |||
|                   void *listener_memory, int32_t listener_memory_size); | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_file(File_Track_System *system, char *name, File_Index *index, File_Time *time); | ||||
| add_listener(File_Track_System *system, char *filename); | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_new_file(File_Track_System *system, char *name, File_Index *index, File_Time *time); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_file_temp_handle(char *name, File_Temp_Handle *handle); | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_from_handle(File_Track_System *system, char *name, File_Temp_Handle handle, File_Index *index, File_Time *time); | ||||
| 
 | ||||
| File_Track_Result | ||||
| finish_with_temp_handle(File_Temp_Handle handle); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_index(File_Track_System *system, char *name, File_Index *index); | ||||
| 
 | ||||
| File_Track_Result | ||||
| stop_tracking_file(File_Track_System *system, File_Index index); | ||||
| 
 | ||||
| File_Track_Result | ||||
| count_tracked_files(File_Track_System *system, int32_t *count); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_time(File_Track_System *system, File_Index index, File_Time *time); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_file_time_now(File_Time *time); | ||||
| remove_listener(File_Track_System *system, char *filename); | ||||
| 
 | ||||
| File_Track_Result | ||||
| move_track_system(File_Track_System *system, void *mem, int32_t size); | ||||
|  | @ -101,21 +51,7 @@ File_Track_Result | |||
| expand_track_system_listeners(File_Track_System *system, void *mem, int32_t size); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_change_event(File_Track_System *system, File_Index *index); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_size(File_Track_System *system, File_Index index, uint32_t *size); | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_data(File_Track_System *system, File_Index index, void *mem, uint32_t size); | ||||
| 
 | ||||
| File_Track_Result | ||||
| rewrite_tracked_file(File_Track_System *system, File_Index index, | ||||
|                      void *data, int32_t size, File_Time *time); | ||||
| 
 | ||||
| File_Track_Result | ||||
| rewrite_arbitrary_file(File_Track_System *system, char *filename, | ||||
|                        void *data, int32_t size, File_Time *time); | ||||
| get_change_event(File_Track_System *system, char *buffer, int32_t max); | ||||
| 
 | ||||
| File_Track_Result | ||||
| shut_down_track_system(File_Track_System *system); | ||||
|  |  | |||
|  | @ -29,6 +29,24 @@ Created on: 20.07.2016 | |||
| # define NotImplemented Assert(!"not implemented") | ||||
| #endif | ||||
| 
 | ||||
| typedef struct{ | ||||
|     uint32_t id[4]; | ||||
| } File_Index; | ||||
| 
 | ||||
| static File_Index | ||||
| zero_file_index(){ | ||||
|     File_Index a = {0}; | ||||
|     return(a); | ||||
| } | ||||
| 
 | ||||
| static int32_t | ||||
| file_index_eq(File_Index a, File_Index b){ | ||||
|     return ((a.id[0] == b.id[0]) && | ||||
|             (a.id[1] == b.id[1]) && | ||||
|             (a.id[2] == b.id[2]) && | ||||
|             (a.id[3] == b.id[3])); | ||||
| } | ||||
| 
 | ||||
| typedef uint32_t rptr32; | ||||
| 
 | ||||
| typedef struct DLL_Node { | ||||
|  | @ -42,6 +60,7 @@ typedef struct { | |||
|     int32_t user_count; | ||||
|      | ||||
|     char dir_name[512]; | ||||
|     int32_t dir_name_len; | ||||
|      | ||||
|     // TODO(allen): I am only ever using one thread
 | ||||
|     // for reading results.  So is it possible to
 | ||||
|  | @ -58,7 +77,6 @@ typedef struct { | |||
| 
 | ||||
| typedef struct { | ||||
|     HANDLE iocp; | ||||
|     HANDLE thread; | ||||
|     CRITICAL_SECTION table_lock; | ||||
|      | ||||
|     void *tables; | ||||
|  | @ -69,32 +87,17 @@ typedef struct { | |||
| typedef struct { | ||||
|     int32_t size; | ||||
|     uint32_t tracked_count; | ||||
|     uint32_t tracked_dir_count; | ||||
|     uint32_t max; | ||||
|     uint32_t change_write_pos; | ||||
|     uint32_t change_read_pos; | ||||
|     rptr32 change_queue; | ||||
|     rptr32 file_table; | ||||
| } File_Track_Tables; | ||||
| 
 | ||||
| typedef struct { | ||||
|     HANDLE file, dir; | ||||
|     HANDLE dir; | ||||
|     File_Index hash; | ||||
|     union { | ||||
|         Directory_Listener_Node *listener_node; | ||||
|         struct { | ||||
|             int32_t change_pos; | ||||
|             int32_t skip_change; | ||||
|         }; | ||||
|     }; | ||||
|     Directory_Listener_Node *listener_node; | ||||
| } File_Track_Entry; | ||||
| 
 | ||||
| typedef struct { | ||||
|     File_Index index; | ||||
|     int32_t still_active; | ||||
| } File_Change_Record; | ||||
| 
 | ||||
| #define FILE_ENTRY_COST (sizeof(File_Change_Record) + sizeof(File_Track_Entry)) | ||||
| #define FILE_ENTRY_COST (sizeof(File_Track_Entry)) | ||||
| 
 | ||||
| #define to_vars_(s) ((File_Track_Vars*)(s)) | ||||
| #define to_tables(v) ((File_Track_Tables*)(v->tables)) | ||||
|  | @ -220,12 +223,7 @@ get_file_entry(File_Track_Tables *tables, File_Index index){ | |||
|     return(entry); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| internal_get_tracked_file_index(File_Track_Tables *tables, | ||||
|                                 char *name, | ||||
|                                 File_Index *index, | ||||
|                                 File_Track_Entry **entry); | ||||
| 
 | ||||
| #if 0 | ||||
| static DWORD | ||||
| directory_watching(LPVOID ptr){ | ||||
|     File_Track_Vars *vars = to_vars_(ptr); | ||||
|  | @ -341,6 +339,7 @@ directory_watching(LPVOID ptr){ | |||
|         } | ||||
|     } | ||||
| } | ||||
| #endif | ||||
| 
 | ||||
| File_Track_Result | ||||
| init_track_system(File_Track_System *system, | ||||
|  | @ -359,14 +358,11 @@ init_track_system(File_Track_System *system, | |||
|         { | ||||
|             tables->size = table_memory_size; | ||||
|             tables->tracked_count = 0; | ||||
|             tables->tracked_dir_count = 0; | ||||
|              | ||||
|             int32_t likely_entry_size = FILE_ENTRY_COST; | ||||
|             int32_t max_number_of_entries = (table_memory_size - sizeof(*tables)) / likely_entry_size; | ||||
|              | ||||
|             tables->change_queue = sizeof(*tables); | ||||
|             tables->file_table = tables->change_queue + | ||||
|                 sizeof(File_Change_Record)*max_number_of_entries; | ||||
|             tables->file_table = sizeof(*tables); | ||||
|             tables->max = max_number_of_entries; | ||||
|         } | ||||
|          | ||||
|  | @ -381,16 +377,10 @@ init_track_system(File_Track_System *system, | |||
|             } | ||||
|         } | ||||
|          | ||||
|         // NOTE(allen): Launch the file watching thread
 | ||||
|         // NOTE(allen): Prepare the file tracking synchronization objects.
 | ||||
|         { | ||||
|             InitializeCriticalSection(&vars->table_lock); | ||||
|              | ||||
|             vars->iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 1); | ||||
|              | ||||
|             vars->thread = CreateThread(0, 0, | ||||
|                                         directory_watching, | ||||
|                                         system, | ||||
|                                         0, 0); | ||||
|         } | ||||
|          | ||||
|         result = FileTrack_Good; | ||||
|  | @ -432,12 +422,6 @@ internal_get_file_index(BY_HANDLE_FILE_INFORMATION info){ | |||
|     return(hash); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| internal_set_time(File_Time *time, BY_HANDLE_FILE_INFORMATION info){ | ||||
|     *time = (((uint64_t)info.ftLastWriteTime.dwHighDateTime << 32) | | ||||
|              (info.ftLastWriteTime.dwLowDateTime)); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| internal_free_slot(File_Track_Tables *tables, File_Track_Entry *entry){ | ||||
|     Assert(!entry_is_available(entry)); | ||||
|  | @ -452,328 +436,138 @@ internal_free_slot(File_Track_Tables *tables, File_Track_Entry *entry){ | |||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| internal_track_file(File_Track_Vars *vars, File_Track_Tables *tables, | ||||
|                     char *name, HANDLE file, File_Index *index, File_Time *time){ | ||||
|      | ||||
| add_listener(File_Track_System *system, char *filename){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     if (file != INVALID_HANDLE_VALUE){ | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|     { | ||||
|         File_Track_Tables *tables = to_tables(vars); | ||||
|          | ||||
|         // TODO(allen): make this real!
 | ||||
|         char dir_name[1024]; | ||||
|         int32_t dir_name_len = | ||||
|             internal_get_parent_name(dir_name, sizeof(dir_name), name); | ||||
|             internal_get_parent_name(dir_name, sizeof(dir_name), filename); | ||||
|          | ||||
|         HANDLE dir = CreateFile( | ||||
|             dir_name, | ||||
|             FILE_LIST_DIRECTORY, | ||||
|             FILE_SHARE_READ | FILE_SHARE_WRITE, | ||||
|             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | ||||
|             0, | ||||
|             OPEN_EXISTING, | ||||
|             FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, | ||||
|             0); | ||||
|          | ||||
|         if (dir != INVALID_HANDLE_VALUE){ | ||||
|             BY_HANDLE_FILE_INFORMATION info = {0}; | ||||
|             BY_HANDLE_FILE_INFORMATION dir_info = {0}; | ||||
|             DWORD getinfo_result = GetFileInformationByHandle(dir, &dir_info); | ||||
|              | ||||
|             if (GetFileInformationByHandle(dir, &dir_info)){ | ||||
|             if (getinfo_result){ | ||||
|                 File_Index dir_hash = internal_get_file_index(dir_info); | ||||
|                  | ||||
|                 File_Lookup_Result dir_lookup = tracking_system_lookup_entry(tables, dir_hash); | ||||
|                  | ||||
|                 if (GetFileInformationByHandle(file, &info)){ | ||||
|                     File_Index hash = internal_get_file_index(info); | ||||
|                      | ||||
|                     if (entry_is_available(dir_lookup.entry)){ | ||||
|                         if (tracking_system_has_space(tables, 2)){ | ||||
|                             Directory_Listener_Node *node = (Directory_Listener_Node*) | ||||
|                                 allocate_node(&vars->free_sentinel); | ||||
|                             if (node){ | ||||
|                                 if (CreateIoCompletionPort(dir, vars->iocp, (ULONG_PTR)node, 1)){ | ||||
|                                     ZeroStruct(node->listener.overlapped); | ||||
|                                     if (ReadDirectoryChangesW(dir, | ||||
|                                                               node->listener.result, | ||||
|                                                               sizeof(node->listener.result), | ||||
|                                                               0, | ||||
|                                                               FILE_NOTIFY_CHANGE_LAST_WRITE, | ||||
|                                                               0, | ||||
|                                                               &node->listener.overlapped, | ||||
|                                                               0)){ | ||||
|                                         node->listener.dir = dir; | ||||
|                                         node->listener.user_count = 1; | ||||
|                                          | ||||
|                                         // TODO(allen): make this real!
 | ||||
|                                         Assert(dir_name_len < sizeof(node->listener.dir_name)); | ||||
|                                         for (int32_t i = 0; i < dir_name_len; ++i){ | ||||
|                                             node->listener.dir_name[i] = dir_name[i]; | ||||
|                                         } | ||||
|                                         node->listener.dir_name[dir_name_len] = 0; | ||||
|                                          | ||||
|                                         dir_lookup.entry->hash = dir_hash; | ||||
|                                         dir_lookup.entry->file = dir; | ||||
|                                         dir_lookup.entry->dir = dir; | ||||
|                                         dir_lookup.entry->listener_node = node; | ||||
|                                         ++tables->tracked_count; | ||||
|                                         ++tables->tracked_dir_count; | ||||
|                                          | ||||
|                                         File_Lookup_Result lookup = | ||||
|                                             tracking_system_lookup_entry(tables, hash); | ||||
|                                         Assert(entry_is_available(lookup.entry)); | ||||
|                                          | ||||
|                                         lookup.entry->hash = hash; | ||||
|                                         lookup.entry->file = file; | ||||
|                                         lookup.entry->dir = node->listener.dir; | ||||
|                                         lookup.entry->change_pos = -1; | ||||
|                                         ++tables->tracked_count; | ||||
|                                          | ||||
|                                         *index = hash; | ||||
|                                         internal_set_time(time, info); | ||||
|                                     } | ||||
|                                     else{ | ||||
|                                         result = FileTrack_FileSystemError; | ||||
|                 if (entry_is_available(dir_lookup.entry)){ | ||||
|                     if (tracking_system_has_space(tables, 1)){ | ||||
|                         Directory_Listener_Node *node = (Directory_Listener_Node*) | ||||
|                             allocate_node(&vars->free_sentinel); | ||||
|                         if (node){ | ||||
|                             if (CreateIoCompletionPort(dir, vars->iocp, (ULONG_PTR)node, 1)){ | ||||
|                                 ZeroStruct(node->listener.overlapped); | ||||
|                                 if (ReadDirectoryChangesW(dir, | ||||
|                                                           node->listener.result, | ||||
|                                                           sizeof(node->listener.result), | ||||
|                                                           0, | ||||
|                                                           FILE_NOTIFY_CHANGE_LAST_WRITE, | ||||
|                                                           0, | ||||
|                                                           &node->listener.overlapped, | ||||
|                                                           0)){ | ||||
|                                     node->listener.dir = dir; | ||||
|                                     node->listener.user_count = 1; | ||||
|                                      | ||||
|                                     // TODO(allen): make this real!
 | ||||
|                                     Assert(dir_name_len < sizeof(node->listener.dir_name)); | ||||
|                                     for (int32_t i = 0; i < dir_name_len; ++i){ | ||||
|                                         node->listener.dir_name[i] = dir_name[i]; | ||||
|                                     } | ||||
|                                     node->listener.dir_name[dir_name_len] = 0; | ||||
|                                     node->listener.dir_name_len = dir_name_len; | ||||
|                                      | ||||
|                                     dir_lookup.entry->hash = dir_hash; | ||||
|                                     dir_lookup.entry->dir = dir; | ||||
|                                     dir_lookup.entry->listener_node = node; | ||||
|                                     ++tables->tracked_count; | ||||
|                                 } | ||||
|                                 else{ | ||||
|                                     result = FileTrack_FileSystemError; | ||||
|                                 } | ||||
|                                  | ||||
|                                 if (result != FileTrack_Good){ | ||||
|                                     insert_node(&vars->free_sentinel, &node->node); | ||||
|                                 } | ||||
|                             } | ||||
|                             else{ | ||||
|                                 result = FileTrack_OutOfListenerMemory; | ||||
|                                 result = FileTrack_FileSystemError; | ||||
|                             } | ||||
|                              | ||||
|                             if (result != FileTrack_Good){ | ||||
|                                 insert_node(&vars->free_sentinel, &node->node); | ||||
|                             } | ||||
|                         } | ||||
|                         else{ | ||||
|                             result = FileTrack_OutOfTableMemory; | ||||
|                             result = FileTrack_OutOfListenerMemory; | ||||
|                         } | ||||
|                     } | ||||
|                     else{ | ||||
|                         if (tracking_system_has_space(tables, 1)){ | ||||
|                             File_Lookup_Result lookup = tracking_system_lookup_entry(tables, hash); | ||||
|                             if (entry_is_available(lookup.entry)){ | ||||
|                                 Directory_Listener_Node *node = dir_lookup.entry->listener_node; | ||||
|                                 ++node->listener.user_count; | ||||
|                                  | ||||
|                                 lookup.entry->hash = hash; | ||||
|                                 lookup.entry->file = file; | ||||
|                                 lookup.entry->dir = node->listener.dir; | ||||
|                                 lookup.entry->change_pos = -1; | ||||
|                                 ++tables->tracked_count; | ||||
|                                  | ||||
|                                 *index = hash; | ||||
|                                 internal_set_time(time, info); | ||||
|                             } | ||||
|                             else{ | ||||
|                                 result = FileTrack_FileAlreadyTracked; | ||||
|                             } | ||||
|                         } | ||||
|                         else{ | ||||
|                             result = FileTrack_OutOfTableMemory; | ||||
|                         } | ||||
|                         result = FileTrack_OutOfTableMemory; | ||||
|                     } | ||||
|                 } | ||||
|                 else{ | ||||
|                     result = FileTrack_FileSystemError; | ||||
|                     Directory_Listener_Node *node = dir_lookup.entry->listener_node; | ||||
|                     ++node->listener.user_count; | ||||
|                 } | ||||
|             } | ||||
|             else{ | ||||
|                 result = FileTrack_FileSystemError; | ||||
|             } | ||||
|              | ||||
|             if (result != FileTrack_Good && dir != 0){ | ||||
|                 CloseHandle(dir); | ||||
|             } | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileSystemError; | ||||
|         } | ||||
|          | ||||
|         if (result != FileTrack_Good && dir != 0 && dir != INVALID_HANDLE_VALUE){ | ||||
|             CloseHandle(dir); | ||||
|         } | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotFound; | ||||
|     } | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| HANDLE | ||||
| open_file_best_privledges(char *name, DWORD open_disposition){ | ||||
|      | ||||
|     // TODO(allen): Try multiple ways to open the
 | ||||
|     // file and get as many privledges as possible.
 | ||||
|     // Expand the API to be capable of reporting
 | ||||
|     // what privledges are available on a file.
 | ||||
|     HANDLE file = CreateFile( | ||||
|         name, | ||||
|         GENERIC_READ | GENERIC_WRITE, | ||||
|         FILE_SHARE_READ | FILE_SHARE_WRITE, | ||||
|         0, | ||||
|         open_disposition, | ||||
|         FILE_ATTRIBUTE_NORMAL, | ||||
|         0); | ||||
|      | ||||
|     return(file); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_file(File_Track_System *system, | ||||
|                     char *name, | ||||
|                     File_Index *index, | ||||
|                     File_Time *time){ | ||||
| remove_listener(File_Track_System *system, char *filename){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     { | ||||
|         File_Track_Tables *tables = to_tables(vars); | ||||
|         HANDLE file = open_file_best_privledges(name, OPEN_EXISTING); | ||||
|         result = internal_track_file(vars, tables, name, file, index, time); | ||||
|         if (file != INVALID_HANDLE_VALUE && result != FileTrack_Good){ | ||||
|             CloseHandle(file); | ||||
|         } | ||||
|     } | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_new_file(File_Track_System *system, char *name, File_Index *index, File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|     { | ||||
|         File_Track_Tables *tables = to_tables(vars); | ||||
|         HANDLE file = open_file_best_privledges(name, CREATE_ALWAYS); | ||||
|         result = internal_track_file(vars, tables, name, file, index, time); | ||||
|         if (file != INVALID_HANDLE_VALUE && result != FileTrack_Good){ | ||||
|             CloseHandle(file); | ||||
|         } | ||||
|     } | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_file_temp_handle(char *name, File_Temp_Handle *handle){ | ||||
|     File_Track_Result result = FileTrack_FileNotFound; | ||||
|      | ||||
|     HANDLE h = open_file_best_privledges(name, OPEN_EXISTING); | ||||
|     if (h != INVALID_HANDLE_VALUE){ | ||||
|         *(HANDLE*)handle = h; | ||||
|         result = FileTrack_Good; | ||||
|     } | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| begin_tracking_from_handle(File_Track_System *system, char *name, File_Temp_Handle handle, | ||||
|                            File_Index *index, File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|     { | ||||
|         File_Track_Tables *tables = to_tables(vars); | ||||
|         HANDLE file = *(HANDLE*)(&handle); | ||||
|         result = internal_track_file(vars, tables, name, file, index, time); | ||||
|     } | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| finish_with_temp_handle(File_Temp_Handle handle){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|      | ||||
|     HANDLE file = *(HANDLE*)(&handle); | ||||
|     CloseHandle(file); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| internal_get_tracked_file_index(File_Track_Tables *tables, | ||||
|                                 char *name, | ||||
|                                 File_Index *index, | ||||
|                                 File_Track_Entry **entry){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|      | ||||
|     // TODO(allen): Can I open this file with no permissions?
 | ||||
|     HANDLE file = CreateFile( | ||||
|         name, | ||||
|         GENERIC_READ | GENERIC_WRITE, | ||||
|         FILE_SHARE_READ | FILE_SHARE_WRITE, | ||||
|         0, | ||||
|         OPEN_EXISTING, | ||||
|         FILE_ATTRIBUTE_NORMAL, | ||||
|         0); | ||||
|      | ||||
|     if (file != INVALID_HANDLE_VALUE){ | ||||
|         BY_HANDLE_FILE_INFORMATION info = {0}; | ||||
|         if (GetFileInformationByHandle(file, &info)){ | ||||
|             File_Index hash = internal_get_file_index(info); | ||||
|              | ||||
|             File_Lookup_Result lookup = tracking_system_lookup_entry(tables, hash); | ||||
|             if (!entry_is_available(lookup.entry)){ | ||||
|                 *index = hash; | ||||
|                 *entry = lookup.entry; | ||||
|             } | ||||
|             else{ | ||||
|                 result = FileTrack_FileNotTracked; | ||||
|             } | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileSystemError; | ||||
|         } | ||||
|         CloseHandle(file); | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotFound; | ||||
|     } | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_index(File_Track_System *system, | ||||
|                        char *name, | ||||
|                        File_Index *index){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|     File_Track_Tables *tables = 0; | ||||
|     File_Track_Entry *entry = 0; | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|     tables = to_tables(vars); | ||||
|     result = internal_get_tracked_file_index(tables, name, index, &entry); | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| stop_tracking_file(File_Track_System *system, File_Index index){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     File_Track_Entry *entry = get_file_entry(tables, index); | ||||
|      | ||||
|     if (entry){ | ||||
|         HANDLE dir = entry->dir; | ||||
|         if (dir != entry->file){ | ||||
|          | ||||
|         // TODO(allen): make this real!
 | ||||
|         char dir_name[1024]; | ||||
|         internal_get_parent_name(dir_name, sizeof(dir_name), filename); | ||||
|          | ||||
|         HANDLE dir = CreateFile( | ||||
|             dir_name, | ||||
|             FILE_LIST_DIRECTORY, | ||||
|             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | ||||
|             0, | ||||
|             OPEN_EXISTING, | ||||
|             FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, | ||||
|             0); | ||||
|          | ||||
|         if (dir != INVALID_HANDLE_VALUE){ | ||||
|             BY_HANDLE_FILE_INFORMATION dir_info = {0}; | ||||
|             if (GetFileInformationByHandle(dir, &dir_info)){ | ||||
|             DWORD getinfo_result = GetFileInformationByHandle(dir, &dir_info); | ||||
|              | ||||
|             if (getinfo_result){ | ||||
|                 File_Index dir_hash =  internal_get_file_index(dir_info); | ||||
|                  | ||||
|                 File_Lookup_Result dir_lookup = tracking_system_lookup_entry(tables, dir_hash); | ||||
|                  | ||||
|                 Assert(!entry_is_available(dir_lookup.entry)); | ||||
|  | @ -782,83 +576,26 @@ stop_tracking_file(File_Track_System *system, File_Index index){ | |||
|                  | ||||
|                 if (node->listener.user_count == 0){ | ||||
|                     insert_node(&vars->free_sentinel, &node->node); | ||||
|                     CloseHandle(entry->dir); | ||||
|                     CloseHandle(dir_lookup.entry->dir); | ||||
|                     internal_free_slot(tables, dir_lookup.entry); | ||||
|                     --tables->tracked_dir_count; | ||||
|                 } | ||||
|                  | ||||
|                 CloseHandle(entry->file); | ||||
|                 internal_free_slot(tables, entry); | ||||
|             } | ||||
|             else{ | ||||
|                 result = FileTrack_FileSystemError; | ||||
|             } | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileNotTracked; | ||||
|         } | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotTracked; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| count_tracked_files(File_Track_System *system, int32_t *count){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Tables *tables = to_tables(to_vars_(system)); | ||||
|     *count = tables->tracked_count - tables->tracked_dir_count; | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_time(File_Track_System *system, File_Index index, File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     File_Track_Entry *entry = get_file_entry(tables, index); | ||||
|      | ||||
|     if (entry){ | ||||
|         BY_HANDLE_FILE_INFORMATION info = {0}; | ||||
|         if (GetFileInformationByHandle(entry->file, &info)){ | ||||
|             internal_set_time(time, info); | ||||
|              | ||||
|             CloseHandle(dir); | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileSystemError; | ||||
|         } | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotTracked; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_file_time_now(File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|      | ||||
|     FILETIME file_time; | ||||
|     SYSTEMTIME system_time; | ||||
|      | ||||
|     GetSystemTime(&system_time); | ||||
|     SystemTimeToFileTime(&system_time, &file_time); | ||||
|     *time = (((uint64_t)file_time.dwHighDateTime << 32) | | ||||
|              (file_time.dwLowDateTime)); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| move_track_system(File_Track_System *system, void *mem, int32_t size){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|  | @ -878,49 +615,13 @@ move_track_system(File_Track_System *system, void *mem, int32_t size){ | |||
|             int32_t likely_entry_size = FILE_ENTRY_COST; | ||||
|             int32_t max_number_of_entries = (size - sizeof(*tables)) / likely_entry_size; | ||||
|              | ||||
|             tables->change_queue = sizeof(*tables); | ||||
|             tables->file_table = tables->change_queue + | ||||
|                 sizeof(File_Change_Record)*max_number_of_entries; | ||||
|             tables->file_table = sizeof(*tables); | ||||
|             tables->max = max_number_of_entries; | ||||
|         } | ||||
|          | ||||
|         if (tables->max > original_tables->max){ | ||||
|             uint32_t original_max = original_tables->max; | ||||
|              | ||||
|             uint32_t change_shift = 0; | ||||
|             uint32_t change_modulo = original_max; | ||||
|              | ||||
|             // NOTE(allen): Copy the original change queue
 | ||||
|             { | ||||
|                 uint32_t start_pos = original_tables->change_read_pos; | ||||
|                 uint32_t end_pos = original_tables->change_write_pos; | ||||
|                  | ||||
|                 uint32_t changes_count = 0; | ||||
|                 if (original_tables->change_write_pos < original_tables->change_read_pos){ | ||||
|                     changes_count = original_max + end_pos - start_pos; | ||||
|                 } | ||||
|                 else{ | ||||
|                     changes_count = end_pos - start_pos; | ||||
|                 } | ||||
|                  | ||||
|                 change_shift = original_max - start_pos; | ||||
|                  | ||||
|                 File_Change_Record *src = (File_Change_Record*) | ||||
|                     to_ptr(original_tables, original_tables->change_queue); | ||||
|                  | ||||
|                 File_Change_Record *dst = (File_Change_Record*) | ||||
|                     to_ptr(tables, tables->change_queue); | ||||
|                  | ||||
|                 for (uint32_t i_dst = 0, i_src = start_pos; | ||||
|                      i_dst < changes_count; | ||||
|                      ++i_dst, i_src = (i_src+1)%original_max){ | ||||
|                     dst[i_dst] = src[i_src]; | ||||
|                 } | ||||
|                  | ||||
|                 tables->change_read_pos = 0; | ||||
|                 tables->change_write_pos = changes_count; | ||||
|             } | ||||
|              | ||||
|             // NOTE(allen): Rehash the tracking table
 | ||||
|             { | ||||
|                 File_Track_Entry *entries = (File_Track_Entry*) | ||||
|  | @ -937,17 +638,10 @@ move_track_system(File_Track_System *system, void *mem, int32_t size){ | |||
|                         Assert(entry_is_available(lookup.entry)); | ||||
|                          | ||||
|                         *lookup.entry = *entry; | ||||
|                          | ||||
|                         if (lookup.entry->file != lookup.entry->dir && | ||||
|                             lookup.entry->change_pos != -1){ | ||||
|                             lookup.entry->change_pos = | ||||
|                                 (lookup.entry->change_pos+change_shift)%change_modulo; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 tables->tracked_count = original_tables->tracked_count; | ||||
|                 tables->tracked_dir_count = original_tables->tracked_dir_count; | ||||
|             } | ||||
|              | ||||
|             // NOTE(allen): Update to the new table
 | ||||
|  | @ -990,189 +684,84 @@ expand_track_system_listeners(File_Track_System *system, void *mem, int32_t size | |||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_change_event(File_Track_System *system, File_Index *index){ | ||||
| get_change_event(File_Track_System *system, char *buffer, int32_t max){ | ||||
|     File_Track_Result result = FileTrack_NoMoreEvents; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     uint32_t write_pos = tables->change_write_pos; | ||||
|     uint32_t read_pos = tables->change_read_pos; | ||||
|     uint32_t max = tables->max; | ||||
|      | ||||
|     File_Change_Record *change_queue = | ||||
|         (File_Change_Record*)to_ptr(tables, tables->change_queue); | ||||
|      | ||||
|     while (read_pos != write_pos){ | ||||
|         File_Change_Record *record = change_queue + read_pos; | ||||
|     { | ||||
|         OVERLAPPED *overlapped = 0; | ||||
|         DWORD length = 0; | ||||
|         ULONG_PTR key = 0; | ||||
|          | ||||
|         read_pos = (read_pos + 1) % max; | ||||
|          | ||||
|         if (record->still_active){ | ||||
|             File_Lookup_Result lookup = tracking_system_lookup_entry(tables, record->index); | ||||
|             if (!entry_is_available(lookup.entry)){ | ||||
|                 lookup.entry->change_pos = -1; | ||||
|                 *index = record->index; | ||||
|                 result = FileTrack_Good; | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     tables->change_write_pos = write_pos; | ||||
|     tables->change_read_pos = read_pos; | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_size(File_Track_System *system, File_Index index, uint32_t *size){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     File_Lookup_Result lookup = tracking_system_lookup_entry(tables, index); | ||||
|     if (!entry_is_available(lookup.entry)){ | ||||
|         DWORD lo, hi; | ||||
|         lo = GetFileSize(lookup.entry->file, &hi); | ||||
|         Assert(hi == 0); | ||||
|         *size = lo; | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotTracked; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| get_tracked_file_data(File_Track_System *system, File_Index index, void *mem, uint32_t size){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     File_Lookup_Result lookup = tracking_system_lookup_entry(tables, index); | ||||
|     if (!entry_is_available(lookup.entry)){ | ||||
|         DWORD read_size = 0; | ||||
|         if (ReadFile(lookup.entry->file, mem, size, &read_size, 0)){ | ||||
|             if (read_size != size){ | ||||
|                 result = FileTrack_FileSystemError; | ||||
|             } | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileSystemError; | ||||
|         } | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotTracked; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| rewrite_tracked_file(File_Track_System *system, File_Index index, | ||||
|                      void *data, int32_t size, File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|     File_Track_Vars *vars = to_vars_(system); | ||||
|      | ||||
|     EnterCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     File_Track_Tables *tables = to_tables(vars); | ||||
|      | ||||
|     File_Lookup_Result lookup = tracking_system_lookup_entry(tables, index); | ||||
|     if (!entry_is_available(lookup.entry)){ | ||||
|         DWORD written = 0; | ||||
|          | ||||
|         lookup.entry->skip_change = 1; | ||||
|         SetFilePointer(lookup.entry->file, 0, 0, FILE_BEGIN); | ||||
|         if (WriteFile(lookup.entry->file, data, size, &written, 0)){ | ||||
|             FILETIME file_time; | ||||
|             SYSTEMTIME system_time; | ||||
|         if (GetQueuedCompletionStatus(vars->iocp, | ||||
|                                       &length, | ||||
|                                       &key, | ||||
|                                       &overlapped, | ||||
|                                       0)){ | ||||
|              | ||||
|             GetSystemTime(&system_time); | ||||
|             SystemTimeToFileTime(&system_time, &file_time); | ||||
|             SetFileTime(lookup.entry->file, 0, 0, &file_time); | ||||
|             Directory_Listener *listener_ptr = (Directory_Listener*)overlapped; | ||||
|             Directory_Listener listener = *listener_ptr; | ||||
|              | ||||
|             FlushFileBuffers(lookup.entry->file); | ||||
|         } | ||||
|         else{ | ||||
|             result = FileTrack_FileSystemError; | ||||
|         } | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotTracked; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
| File_Track_Result | ||||
| rewrite_arbitrary_file(File_Track_System *system, char *filename, | ||||
|                        void *data, int32_t size, File_Time *time){ | ||||
|     File_Track_Result result = FileTrack_Good; | ||||
|      | ||||
|     HANDLE file = CreateFile( | ||||
|         filename, | ||||
|         GENERIC_WRITE, | ||||
|         FILE_SHARE_READ | FILE_SHARE_WRITE, | ||||
|         0, | ||||
|         OPEN_EXISTING, | ||||
|         FILE_ATTRIBUTE_NORMAL, | ||||
|         0); | ||||
|      | ||||
|     if (file != INVALID_HANDLE_VALUE){ | ||||
|         File_Track_Vars *vars = to_vars_(system); | ||||
|         EnterCriticalSection(&vars->table_lock); | ||||
|         { | ||||
|             File_Track_Tables *tables = to_tables(vars); | ||||
|             BY_HANDLE_FILE_INFORMATION info = {0}; | ||||
|             if (GetFileInformationByHandle(file, &info)){ | ||||
|                 DWORD written = 0; | ||||
|             ZeroStruct(listener_ptr->overlapped); | ||||
|             ReadDirectoryChangesW(listener_ptr->dir, | ||||
|                                   listener_ptr->result, | ||||
|                                   sizeof(listener_ptr->result), | ||||
|                                   0, | ||||
|                                   FILE_NOTIFY_CHANGE_LAST_WRITE, | ||||
|                                   0, | ||||
|                                   &listener_ptr->overlapped, | ||||
|                                   0); | ||||
|              | ||||
|             char *listener_buffer = listener.result; | ||||
|             DWORD offset = 0; | ||||
|             FILE_NOTIFY_INFORMATION *info = 0; | ||||
|              | ||||
|             for (;;){ | ||||
|                 info = (FILE_NOTIFY_INFORMATION*)(listener_buffer + offset); | ||||
|                  | ||||
|                 File_Index index = internal_get_file_index(info); | ||||
|                 File_Lookup_Result lookup = tracking_system_lookup_entry(tables, index); | ||||
|                 if (!entry_is_available(lookup.entry)){ | ||||
|                     lookup.entry->skip_change = 1; | ||||
|                 int32_t len = info->FileNameLength / 2; | ||||
|                 if (listener.dir_name_len + 1 + len < max){ | ||||
|                     int32_t pos = 0; | ||||
|                     char *src = listener.dir_name; | ||||
|                     for (int32_t i = 0; src[i]; ++i, ++pos){ | ||||
|                         buffer[pos] = src[i]; | ||||
|                     } | ||||
|                      | ||||
|                     buffer[pos++] = '/'; | ||||
|                      | ||||
|                     for (int32_t i = 0; i < len; ++i, ++pos){ | ||||
|                         buffer[pos] = (char)info->FileName[i]; | ||||
|                     } | ||||
|                     buffer[pos] = 0; | ||||
|                      | ||||
|                     result = FileTrack_Good; | ||||
|                 } | ||||
|                 else{ | ||||
|                     // TODO(allen): Need some way to stash this result so that if the
 | ||||
|                     // user comes back with more memory we can give them the change
 | ||||
|                     // notification they missed.
 | ||||
|                     result = FileTrack_MemoryTooSmall; | ||||
|                 } | ||||
|                  | ||||
|                 WriteFile(file, data, size, &written, 0); | ||||
|                  | ||||
|                 FILETIME file_time; | ||||
|                 SYSTEMTIME system_time; | ||||
|                  | ||||
|                 GetSystemTime(&system_time); | ||||
|                 SystemTimeToFileTime(&system_time, &file_time); | ||||
|                 SetFileTime(lookup.entry->file, 0, 0, &file_time); | ||||
|             } | ||||
|             else{ | ||||
|                 result = FileTrack_FileSystemError; | ||||
|                 if (info->NextEntryOffset != 0){ | ||||
|                     // TODO(allen): We're not ready to handle this yet.
 | ||||
|                     // For now I am breaking.  In the future, if there
 | ||||
|                     // are more results we should stash them and return
 | ||||
|                     // them in future calls.
 | ||||
|                     offset += info->NextEntryOffset; | ||||
|                     break; | ||||
|                 } | ||||
|                 else{ | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         LeaveCriticalSection(&vars->table_lock); | ||||
|          | ||||
|         CloseHandle(file); | ||||
|     } | ||||
|     else{ | ||||
|         result = FileTrack_FileNotFound; | ||||
|     } | ||||
|      | ||||
|     LeaveCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
|  | @ -1193,7 +782,7 @@ shut_down_track_system(File_Track_System *system){ | |||
|     for (; index < max; ++index){ | ||||
|         File_Track_Entry *entry = entries + index; | ||||
|         if (!entry_is_available(entry)){ | ||||
|             if (!CloseHandle(entry->file)){ | ||||
|             if (!CloseHandle(entry->dir)){ | ||||
|                 win32_result = 1; | ||||
|             } | ||||
|         } | ||||
|  | @ -1202,17 +791,13 @@ shut_down_track_system(File_Track_System *system){ | |||
|     if (!CloseHandle(vars->iocp)){ | ||||
|         win32_result = 1; | ||||
|     } | ||||
|     TerminateThread(vars->thread, 0); | ||||
|     if (!CloseHandle(vars->thread)){ | ||||
|         win32_result = 1; | ||||
|     } | ||||
|      | ||||
|     DeleteCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     if (win32_result){ | ||||
|         result = FileTrack_FileSystemError; | ||||
|     } | ||||
|      | ||||
|     DeleteCriticalSection(&vars->table_lock); | ||||
|      | ||||
|     return(result); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,436 +0,0 @@ | |||
| /*
 | ||||
| 
 | ||||
| Copy Right FourTech LLC, 2016 | ||||
| All Rights Are Reserved | ||||
| 
 | ||||
| A test bed for a cross platform file tracking reliability layer. | ||||
| Developed for the use cases in 4coder, but I anticipate that this | ||||
| will be a general problem for me. - Allen Webster | ||||
| 
 | ||||
| Created on: 20.07.2016 | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| // TOP
 | ||||
| 
 | ||||
| #define FILE_TRACK_MAIN | ||||
| 
 | ||||
| #include "4tech_file_track.h" | ||||
| #include "4tech_file_track_win32.c" | ||||
| 
 | ||||
| #include "filetrack_test.c" | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| #include <assert.h> | ||||
| #include <string.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdio.h> | ||||
| 
 | ||||
| #define FILE_TRACK_TEST_DIR1 "w:/filetrack/data/" | ||||
| #define FILE_TRACK_TEST_DIR2 "w:/filetrack/data2/" | ||||
| 
 | ||||
| #define FAKE_TRACK_TEST_DIR "w:/filetrack/data1000/" | ||||
| 
 | ||||
| #define ALT_NAME_TEST_DIR1 "c:/work/filetrack/data/" | ||||
| #define ALT_NAME_TEST_DIR2 "c:/work/filetrack/data2/" | ||||
| 
 | ||||
| static char * test_files[] = { | ||||
|     FILE_TRACK_TEST_DIR1"autotab.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"basic.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"basic.txt", | ||||
|     FILE_TRACK_TEST_DIR1"cleanme.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"emptyfile.txt", | ||||
|     FILE_TRACK_TEST_DIR1"lexer_test.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"lexer_test2.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"lexer_test3.cpp", | ||||
|     FILE_TRACK_TEST_DIR1"saveas.txt", | ||||
|     FILE_TRACK_TEST_DIR1"test_large.cpp", | ||||
|      | ||||
|     FILE_TRACK_TEST_DIR2"autotab.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"basic.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"basic.txt", | ||||
|     FILE_TRACK_TEST_DIR2"cleanme.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"emptyfile.txt", | ||||
|     FILE_TRACK_TEST_DIR2"lexer_test.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"lexer_test2.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"lexer_test3.cpp", | ||||
|     FILE_TRACK_TEST_DIR2"saveas.txt", | ||||
|     FILE_TRACK_TEST_DIR2"test_large.cpp", | ||||
| }; | ||||
| 
 | ||||
| static char * test_alt_files[] = { | ||||
|     ALT_NAME_TEST_DIR1"autotab.cpp", | ||||
|     ALT_NAME_TEST_DIR1"basic.cpp", | ||||
|     ALT_NAME_TEST_DIR1"basic.txt", | ||||
|     ALT_NAME_TEST_DIR1"cleanme.cpp", | ||||
|     ALT_NAME_TEST_DIR1"emptyfile.txt", | ||||
|     ALT_NAME_TEST_DIR1"lexer_test.cpp", | ||||
|     ALT_NAME_TEST_DIR1"lexer_test2.cpp", | ||||
|     ALT_NAME_TEST_DIR1"lexer_test3.cpp", | ||||
|     ALT_NAME_TEST_DIR1"saveas.txt", | ||||
|     ALT_NAME_TEST_DIR1"test_large.cpp", | ||||
|      | ||||
|     ALT_NAME_TEST_DIR2"autotab.cpp", | ||||
|     ALT_NAME_TEST_DIR2"basic.cpp", | ||||
|     ALT_NAME_TEST_DIR2"basic.txt", | ||||
|     ALT_NAME_TEST_DIR2"cleanme.cpp", | ||||
|     ALT_NAME_TEST_DIR2"emptyfile.txt", | ||||
|     ALT_NAME_TEST_DIR2"lexer_test.cpp", | ||||
|     ALT_NAME_TEST_DIR2"lexer_test2.cpp", | ||||
|     ALT_NAME_TEST_DIR2"lexer_test3.cpp", | ||||
|     ALT_NAME_TEST_DIR2"saveas.txt", | ||||
|     ALT_NAME_TEST_DIR2"test_large.cpp", | ||||
| }; | ||||
| 
 | ||||
| static char * fake_files[] = { | ||||
|     FAKE_TRACK_TEST_DIR"autotab.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"basic.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"basic.txt", | ||||
|     FAKE_TRACK_TEST_DIR"cleanme.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"emptyfile.txt", | ||||
|     FAKE_TRACK_TEST_DIR"lexer_test.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"lexer_test2.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"lexer_test3.cpp", | ||||
|     FAKE_TRACK_TEST_DIR"saveas.txt", | ||||
|     FAKE_TRACK_TEST_DIR"test_large.cpp", | ||||
| }; | ||||
| 
 | ||||
| #define ArrayCount(a) ((sizeof(a))/(sizeof(*a))) | ||||
| 
 | ||||
| typedef struct{ | ||||
|     File_Index unique_file_index; | ||||
|     File_Time time; | ||||
| } MyFileThing; | ||||
| 
 | ||||
| void test_body_A(int32_t size1, int32_t size2){ | ||||
|     void *mem1 = malloc(size1); | ||||
|     void *mem2 = malloc(size2); | ||||
|     memset(mem1, 0, size1); | ||||
|      | ||||
|     File_Track_System track = {0}; | ||||
|     int32_t result = init_track_system(&track, | ||||
|                                        mem1, size1, | ||||
|                                        mem2, size2); | ||||
|     assert(result == FileTrack_Good); | ||||
|      | ||||
|     MyFileThing my_file_things[1000]; | ||||
|     memset(my_file_things, 0, sizeof(my_file_things)); | ||||
|      | ||||
|     // NOTE(allen): track in all the test files
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         char *filename = test_files[i]; | ||||
|          | ||||
|         File_Index new_file = zero_file_index(); | ||||
|         File_Time new_time = 0; | ||||
|         int32_t result = begin_tracking_file(&track, filename, &new_file, &new_time); | ||||
|         while (result != FileTrack_Good){ | ||||
|              | ||||
|             switch (result){ | ||||
|                 case FileTrack_OutOfTableMemory: | ||||
|                 { | ||||
|                     int32_t new_mem_size = size1*2; | ||||
|                     void *new_mem = malloc(new_mem_size); | ||||
|                      | ||||
|                     memset(new_mem, 0, new_mem_size); | ||||
|                     move_track_system(&track, new_mem, new_mem_size); | ||||
|                      | ||||
|                     free(mem1); | ||||
|                     size1 = new_mem_size; | ||||
|                     mem1 = new_mem; | ||||
|                 }break; | ||||
|                  | ||||
|                 case FileTrack_OutOfListenerMemory: | ||||
|                 { | ||||
|                     size2 *= 2; | ||||
|                     void *new_mem = malloc(size2); | ||||
|                     memset(new_mem, 0, size2); | ||||
|                     expand_track_system_listeners(&track, new_mem, size2); | ||||
|                 }break; | ||||
|                  | ||||
|                 default: | ||||
|                 { | ||||
|                     Assert(result == FileTrack_Good); | ||||
|                 }break; | ||||
|             } | ||||
|              | ||||
|             result = begin_tracking_file(&track, filename, &new_file, &new_time); | ||||
|         } | ||||
|          | ||||
|         my_file_things[i].unique_file_index = new_file; | ||||
|         my_file_things[i].time = new_time; | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): track in fake directories
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(fake_files); | ||||
|          ++i){ | ||||
|         File_Index new_file = zero_file_index(); | ||||
|         File_Time new_time = 0; | ||||
|          | ||||
|         char *filename = fake_files[i]; | ||||
|          | ||||
|         int32_t result = begin_tracking_file(&track, filename, &new_file, &new_time); | ||||
|         assert(result == FileTrack_FileNotFound); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): track in already tracked files
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Index new_file = zero_file_index(); | ||||
|         File_Time new_time = 0; | ||||
|          | ||||
|         char *filename = test_files[i]; | ||||
|          | ||||
|         int32_t result = begin_tracking_file(&track, filename, &new_file, &new_time); | ||||
|         assert(result == FileTrack_FileAlreadyTracked); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): track in already tracked files via alt-names
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_alt_files); | ||||
|          ++i){ | ||||
|         File_Index new_file = zero_file_index(); | ||||
|         File_Time new_time = 0; | ||||
|          | ||||
|         char *filename = test_alt_files[i]; | ||||
|          | ||||
|         int32_t result = begin_tracking_file(&track, filename, &new_file, &new_time); | ||||
|         assert(result == FileTrack_FileAlreadyTracked); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): each file is still up to date
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Time time = 0; | ||||
|         File_Index index = my_file_things[i].unique_file_index; | ||||
|          | ||||
|         get_tracked_file_time(&track, index, &time); | ||||
|          | ||||
|         assert(time == my_file_things[i].time); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): can still get index from file name
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Index index = my_file_things[i].unique_file_index; | ||||
|          | ||||
|         File_Index result_index1 = zero_file_index(); | ||||
|         char *filename1 = test_files[i]; | ||||
|         get_tracked_file_index(&track, filename1, &result_index1); | ||||
|          | ||||
|         File_Index result_index2 = zero_file_index(); | ||||
|         char *filename2 = test_alt_files[i]; | ||||
|         get_tracked_file_index(&track, filename2, &result_index2); | ||||
|          | ||||
|         assert(file_index_eq(result_index1, index)); | ||||
|         assert(file_index_eq(result_index2, index)); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): rewrite all of the files
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         char *filename = test_files[i]; | ||||
|          | ||||
|         test_rewrite_file_in_child_proc(filename); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): each file is behind
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Time time = 0; | ||||
|         File_Index index = my_file_things[i].unique_file_index; | ||||
|          | ||||
|         get_tracked_file_time(&track, index, &time); | ||||
|          | ||||
|         assert(my_file_things[i].time < time); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): poll the tracking system for changed files
 | ||||
|     for (;;){ | ||||
|         File_Index index = zero_file_index(); | ||||
|         File_Time time = 0; | ||||
|          | ||||
|         int32_t result = get_change_event(&track, &index); | ||||
|          | ||||
|         if (result == FileTrack_NoMoreEvents){ | ||||
|             break; | ||||
|         } | ||||
|          | ||||
|         result = get_tracked_file_time(&track, index, &time); | ||||
|          | ||||
|         for (int32_t i = 0; | ||||
|              i < ArrayCount(test_files); | ||||
|              ++i){ | ||||
|             File_Index my_index = my_file_things[i].unique_file_index; | ||||
|             if (file_index_eq(my_index, index)){ | ||||
|                 my_file_things[i].time = time; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): each file is still up to date (episode 2)
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Time time = 0; | ||||
|         File_Index index = my_file_things[i].unique_file_index; | ||||
|          | ||||
|         get_tracked_file_time(&track, index, &time); | ||||
|          | ||||
|         assert(time == my_file_things[i].time); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): rewrite each file myself
 | ||||
|     for (int32_t i = 0; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Index index = my_file_things[i].unique_file_index; | ||||
|         File_Time time = test_rewrite_file(&track, index); | ||||
|          | ||||
|         my_file_things[i].time = time; | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): check there are no changed file events
 | ||||
|     { | ||||
|         File_Index index = zero_file_index(); | ||||
|          | ||||
|         int32_t result = get_change_event(&track, &index); | ||||
|         assert(result == FileTrack_NoMoreEvents); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): rewrite half of the files twice
 | ||||
|     int32_t mid_point = ArrayCount(test_files) / 2; | ||||
|     for (int32_t i = 0; | ||||
|          i < mid_point; | ||||
|          ++i){ | ||||
|         char *filename = test_files[i]; | ||||
|          | ||||
|         test_rewrite_file_in_child_proc(filename); | ||||
|     } | ||||
|      | ||||
|     for (int32_t i = 0; | ||||
|          i < mid_point; | ||||
|          ++i){ | ||||
|         char *filename = test_files[i]; | ||||
|          | ||||
|         test_rewrite_file_in_child_proc(filename); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): check number of events equals mid_point
 | ||||
|     int32_t count = 0; | ||||
|     for (;;){ | ||||
|         File_Index index = zero_file_index(); | ||||
|         File_Time time = 0; | ||||
|          | ||||
|         int32_t result = get_change_event(&track, &index); | ||||
|          | ||||
|         if (result == FileTrack_NoMoreEvents){ | ||||
|             break; | ||||
|         } | ||||
|          | ||||
|         result = get_tracked_file_time(&track, index, &time); | ||||
|          | ||||
|         ++count; | ||||
|          | ||||
|         for (int32_t i = 0; | ||||
|              i < ArrayCount(test_files); | ||||
|              ++i){ | ||||
|             File_Index my_index = my_file_things[i].unique_file_index; | ||||
|             if (file_index_eq(my_index, index)){ | ||||
|                 my_file_things[i].time = time; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     assert(count == mid_point); | ||||
|      | ||||
|     // NOTE(allen): untrack half of the files
 | ||||
|     for (int32_t i = 0; | ||||
|          i < mid_point; | ||||
|          ++i){ | ||||
|         File_Index stop_file = my_file_things[i].unique_file_index; | ||||
|         stop_tracking_file(&track, stop_file); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): untrack the same files again
 | ||||
|     for (int32_t i = 0; | ||||
|          i < mid_point; | ||||
|          ++i){ | ||||
|         File_Index stop_file = my_file_things[i].unique_file_index; | ||||
|         int32_t result = stop_tracking_file(&track, stop_file); | ||||
|         assert(result == FileTrack_FileNotTracked); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): make sure the number of remaining files is correct
 | ||||
|     { | ||||
|         int32_t track_count = 0; | ||||
|         count_tracked_files(&track, &track_count); | ||||
|         assert(track_count == (ArrayCount(test_files) - mid_point)); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): untrack the rest of the files
 | ||||
|     for (int32_t i = mid_point; | ||||
|          i < ArrayCount(test_files); | ||||
|          ++i){ | ||||
|         File_Index stop_file = my_file_things[i].unique_file_index; | ||||
|         stop_tracking_file(&track, stop_file); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): make sure the system is empty
 | ||||
|     { | ||||
|         int32_t track_count = 0; | ||||
|         count_tracked_files(&track, &track_count); | ||||
|         assert(track_count == 0); | ||||
|     } | ||||
|      | ||||
|     // NOTE(allen): finish using the track system
 | ||||
|     { | ||||
|         int32_t result = shut_down_track_system(&track); | ||||
|         assert(result == FileTrack_Good); | ||||
|     } | ||||
| } | ||||
|                   | ||||
| 
 | ||||
| // NOTE(allen): test basic tracking logic
 | ||||
| void test_1(void){ | ||||
|     int32_t size1 = (16 << 10); | ||||
|     int32_t size2 = (16 << 10); | ||||
|     test_body_A(size1, size2); | ||||
| } | ||||
| 
 | ||||
| // NOTE(allen): test memory expansion system for tables
 | ||||
| void test_2(void){ | ||||
|     int32_t size1 = (1 << 10); | ||||
|     int32_t size2 = (16 << 10); | ||||
|     test_body_A(size1, size2); | ||||
| } | ||||
| 
 | ||||
| // NOTE(allen): test memory expansion system for listening nodes
 | ||||
| void test_3(void){ | ||||
|     int32_t size1 = (16 << 10); | ||||
|     int32_t size2 = (5 << 10); | ||||
|     test_body_A(size1, size2); | ||||
| } | ||||
| 
 | ||||
| // NOTE(allen): test both memory expansion systems
 | ||||
| void test_4(void){ | ||||
|     int32_t size1 = (1 << 10); | ||||
|     int32_t size2 = (5 << 10); | ||||
|     test_body_A(size1, size2); | ||||
| } | ||||
| 
 | ||||
| int main(int argc, char **argv){ | ||||
|     test_4(); | ||||
|     return(0); | ||||
| } | ||||
| 
 | ||||
| // BOTTOM
 | ||||
| 
 | ||||
|  | @ -1,114 +0,0 @@ | |||
| /*
 | ||||
| 
 | ||||
| Copy Right FourTech LLC, 2016 | ||||
| All Rights Are Reserved | ||||
| 
 | ||||
| Helpers for the filetrack_main.c test bed. | ||||
| 
 | ||||
| Created on: 20.07.2016 | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| // TOP
 | ||||
| 
 | ||||
| #define FILE_REWRITER "w:/filetrack/build/file_rewriter" | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdio.h> | ||||
| #include <assert.h> | ||||
| 
 | ||||
| static void | ||||
| rewrite(char *buffer, int32_t size){ | ||||
|     for (int32_t i = 0; | ||||
|          i < size; | ||||
|          ++i){ | ||||
|         if (buffer[i] >= 'a' && buffer[i] < 'z'){ | ||||
|             ++buffer[i]; | ||||
|         } | ||||
|         else if (buffer[i] == 'z'){ | ||||
|             buffer[i] = 'a'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| append(char *buffer, int32_t *pos, char *src){ | ||||
|     int32_t i = *pos; | ||||
|     src -= i; | ||||
|     for (; src[i]; ++i){ | ||||
|         buffer[i] = src[i]; | ||||
|     } | ||||
|     *pos = i; | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| test_rewrite_file_in_child_proc(char *filename){ | ||||
|     char space[2048]; | ||||
|     int32_t pos = 0; | ||||
|      | ||||
|     append(space, &pos, FILE_REWRITER" "); | ||||
|     append(space, &pos, filename); | ||||
|     space[pos] = 0; | ||||
|      | ||||
|     int32_t result = system(space); | ||||
|     assert(result == 0); | ||||
| } | ||||
| 
 | ||||
| #ifndef FILE_TRACK_MAIN | ||||
| 
 | ||||
| int | ||||
| main(int argc, char **argv){ | ||||
|     if (argc == 2){ | ||||
|         char *filename = argv[1]; | ||||
|          | ||||
|         char *mem = 0; | ||||
|         int32_t size = 0; | ||||
|          | ||||
|         FILE *file = fopen(filename, "rb"); | ||||
|         assert(file); | ||||
|         fseek(file, 0, SEEK_END); | ||||
|         size = ftell(file); | ||||
|         fseek(file, 0, SEEK_SET); | ||||
|         mem = (char*)malloc(size+1); | ||||
|         fread(mem, 1, size, file); | ||||
|         fclose(file); | ||||
|          | ||||
|         rewrite(mem, size); | ||||
|          | ||||
|         file = fopen(filename, "wb"); | ||||
|         assert(file); | ||||
|         fwrite(mem, 1, size, file); | ||||
|         fclose(file); | ||||
|     } | ||||
|     return(0); | ||||
| } | ||||
| 
 | ||||
| #else | ||||
| 
 | ||||
| static File_Time | ||||
| test_rewrite_file(File_Track_System *system, File_Index index){ | ||||
|     char *mem = 0; | ||||
|     uint32_t size = 0; | ||||
|     int32_t result = 0; | ||||
|      | ||||
|     result = get_tracked_file_size(system, index, &size); | ||||
|     assert(result == FileTrack_Good); | ||||
|     mem = (char*)malloc(size+1); | ||||
|     result = get_tracked_file_data(system, index, mem, size); | ||||
|     assert(result == FileTrack_Good); | ||||
|      | ||||
|     rewrite(mem, size); | ||||
|      | ||||
|     File_Time time = 0; | ||||
|     rewrite_tracked_file(system, index, mem, size, &time); | ||||
|      | ||||
|     free(mem); | ||||
|      | ||||
|     return(time); | ||||
| } | ||||
| 
 | ||||
| #endif | ||||
| 
 | ||||
| // BOTTOM
 | ||||
| 
 | ||||
		Loading…
	
		Reference in New Issue
	
	 Allen Webster
						Allen Webster