diff --git a/cinera/cinera.c b/cinera/cinera.c index 2beceb5..4775790 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -23,15 +23,22 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, .Minor = 10, - .Patch = 12 + .Patch = 13 }; +#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW +#include // NOTE(matt): open() +#undef __USE_XOPEN2K8 + #include // NOTE(matt): varargs +#define __USE_POSIX // NOTE(matt): fileno() #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror +#undef __USE_POSIX #include // NOTE(matt): calloc, malloc, free #include // NOTE(matt): getopts #include #include +#include // NOTE(matt): flock #include #include #include @@ -41,10 +48,6 @@ version CINERA_APP_VERSION = { #include // NOTE(matt): ioctl and TIOCGWINSZ #include -#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW -#include // NOTE(matt): open() -#undef __USE_XOPEN2K8 - #define __USE_XOPEN2K // NOTE(matt): readlink() #include // NOTE(matt): sleep() #undef __USE_XOPEN2K @@ -67,6 +70,7 @@ version CINERA_APP_VERSION = { typedef uint64_t bool; #define TRUE 1 #define FALSE 0 +#define NA 0 #define SECONDS_PER_HOUR 3600 #define SECONDS_PER_MINUTE 60 @@ -279,6 +283,7 @@ typedef enum RC_ERROR_DIRECTORY, RC_ERROR_FATAL, RC_ERROR_FILE, + RC_ERROR_FILE_LOCKED, RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, @@ -464,6 +469,7 @@ typedef struct buffer Buffer; char *Path; FILE *Handle; + bool Locking; } file; typedef struct @@ -1064,7 +1070,7 @@ ExtendString0(char **Dest, string Src) } file -InitFile(string *Directory, string *Filename, extension_id Extension) +InitFile(string *Directory, string *Filename, extension_id Extension, bool Locking) { file Result = {}; if(Directory) @@ -1079,6 +1085,36 @@ InitFile(string *Directory, string *Filename, extension_id Extension) { ExtendString0(&Result.Path, ExtensionStrings[Extension]); } + Result.Locking = Locking; + return Result; +} + +void +CloseFile(file *F, bool CloseLocking) +{ + if(F->Handle) + { + fclose(F->Handle); + F->Handle = 0; + if(F->Locking && !CloseLocking) + { + F->Handle = fopen(F->Path, "r"); + flock(fileno(F->Handle), LOCK_EX | LOCK_NB); + } + } +} + +bool +TryLock(file *F) // USAGE: File shall already be open, if possible +{ + bool Result = TRUE; + if(F->Locking && F->Handle) + { + if(flock(fileno(F->Handle), LOCK_EX | LOCK_NB) == -1) + { + Result = FALSE; + } + } return Result; } @@ -1088,21 +1124,23 @@ ReadFileIntoBuffer(file *F) rc Result = RC_ERROR_FILE; if(F->Path) { - if((F->Handle = fopen(F->Path, "r"))) + if(F->Handle || (F->Handle = fopen(F->Path, "r"))) { - fseek(F->Handle, 0, SEEK_END); - F->Buffer.Size = ftell(F->Handle); - F->Buffer.Location = malloc(F->Buffer.Size); - F->Buffer.Ptr = F->Buffer.Location; - fseek(F->Handle, 0, SEEK_SET); - fread(F->Buffer.Location, F->Buffer.Size, 1, F->Handle); - fclose(F->Handle); - F->Handle = 0; - Result = RC_SUCCESS; - } - else - { - perror(F->Path); + if(TryLock(F)) + { + fseek(F->Handle, 0, SEEK_END); + F->Buffer.Size = ftell(F->Handle); + F->Buffer.Location = malloc(F->Buffer.Size); + F->Buffer.Ptr = F->Buffer.Location; + fseek(F->Handle, 0, SEEK_SET); + fread(F->Buffer.Location, F->Buffer.Size, 1, F->Handle); + CloseFile(F, FALSE); + Result = RC_SUCCESS; + } + else + { + Result = RC_ERROR_FILE_LOCKED; + } } } return Result; @@ -1914,16 +1952,6 @@ ConfigErrorUnset(config_identifier_id FieldID) "Unset %s\n", ConfigIdentifiers[FieldID].String); } -void -ConfigFileIncludeError(string *Filename, uint64_t LineNumber, string Path) -{ - ErrorFilenameAndLineNumber(Filename, LineNumber, S_WARNING, ED_CONFIG); - fprintf(stderr, - "Included file could not be opened (%s): ", strerror(errno)); - PrintStringC(CS_MAGENTA_BOLD, Path); - fprintf(stderr, "\n"); -} - void ConfigErrorSizing(string *Filename, uint64_t LineNumber, config_identifier_id FieldID, string *Received, uint64_t MaxSize) { @@ -1933,6 +1961,27 @@ ConfigErrorSizing(string *Filename, uint64_t LineNumber, config_identifier_id Fi fprintf(stderr, "\n"); } +void +ConfigErrorLockedConfigLocation(string *Filename, uint64_t LineNumber, string *Received) +{ + ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG); + fprintf(stderr, "%s file %s%.*s%s is in use by another Cinera instance\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]); +} + +void +ConfigErrorUnopenableConfigLocation(string *Filename, uint64_t LineNumber, string *Received) +{ + ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG); + fprintf(stderr, "%s file %s%.*s%s could not be opened: %s\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END], strerror(errno)); +} + +void +ConfigErrorLockedDBLocation(string *Filename, uint64_t LineNumber, string *Received) +{ + ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG); + fprintf(stderr, "File at db_location %s%.*s%s is in use by another Cinera instance\n", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]); +} + void ConfigErrorInt(string *Filename, uint64_t LineNumber, severity Severity, char *Message, uint64_t Number) { @@ -2740,6 +2789,7 @@ typedef struct { file File; file_signposted Metadata; + bool Ready; db_header5 Header; db_block_projects5 ProjectsBlock; @@ -3024,17 +3074,23 @@ ClearTemplateMetadata(template *Template) } void -FreeFile(file *F) +FreeFileBufferAndPath(file *F) { FreeBuffer(&F->Buffer); Free(F->Path); - F->Handle = 0; } void -FreeSignpostedFile(file_signposted *F) +FreeFile(file *F, bool CloseLocking) { - FreeFile(&F->File); + CloseFile(F, CloseLocking); + FreeFileBufferAndPath(F); +} + +void +FreeSignpostedFile(file_signposted *F, bool CloseLocking) +{ + FreeFile(&F->File, CloseLocking); file_signposts Zero = {}; F->Signposts = Zero; } @@ -3042,7 +3098,7 @@ FreeSignpostedFile(file_signposted *F) void FreeTemplate(template *Template) { - FreeFile(&Template->File); + FreeFile(&Template->File, NA); ClearTemplateMetadata(Template); } @@ -3152,8 +3208,6 @@ typedef enum WT_CONFIG, } watch_type; -#include "cinera_config.c" - typedef struct { watch_type Type; @@ -3161,6 +3215,7 @@ typedef struct extension_id Extension; project *Project; asset *Asset; + FILE *Handle; } watch_file; typedef struct @@ -3178,6 +3233,8 @@ typedef struct uint32_t DefaultEventsMask; } watch_handles; +#include "cinera_config.c" + #define AFD 1 #define AFE 0 @@ -4281,17 +4338,17 @@ InitTemplate(template *Template, string Location, template_type Type) Template->Metadata.Type = Type; if(Type == TEMPLATE_GLOBAL_SEARCH) { - Template->File = InitFile(&Config->GlobalTemplatesDir, &Config->GlobalSearchTemplatePath, EXT_NULL); + Template->File = InitFile(&Config->GlobalTemplatesDir, &Config->GlobalSearchTemplatePath, EXT_NULL, FALSE); } else { if(Location.Base[0] == '/') { - Template->File = InitFile(0, &Location, EXT_NULL); + Template->File = InitFile(0, &Location, EXT_NULL, FALSE); } else { - Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL); + Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL, FALSE); } } fprintf(stderr, "%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path); @@ -4516,7 +4573,7 @@ ConstructAssetPath(file *AssetFile, string Filename, asset_type Type) void CycleFile(file *File) { - fclose(File->Handle); + CloseFile(File, FALSE); // TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily? // The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically // If we switch the whole database over to use linked lists - the database being the only file we actually cycle @@ -4541,7 +4598,7 @@ CycleSignpostedFile(file_signposted *File) // NOTE(matt) This file_signposted struct is totally hardcoded to the Metadata file, but may suffice for now // // TODO(matt): This is probably insufficient. We likely need to offset the pointers any time we add stuff to the database - fclose(File->File.Handle); + CloseFile(&File->File, FALSE); // TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily? // The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically // If we switch the whole database over to use linked lists - the database being the only file we actually cycle @@ -4834,9 +4891,7 @@ SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, HTMLFile->Handle = fopen(HTMLFile->Path, "w"); fwrite(HTMLFile->Buffer.Location, HTMLFile->Buffer.Size, 1, HTMLFile->Handle); - fclose(HTMLFile->Handle); - HTMLFile->Handle = 0; - FreeFile(HTMLFile); + FreeFile(HTMLFile, NA); } // TODO(matt): Bounds-check the Metadata.File.Buffer @@ -5136,13 +5191,25 @@ AccumulateFileEditSize(file_signposted *File, int64_t Bytes) File->Signposts.Edit.Size += Bytes; } +FILE * +OpenFileForWriting(file *F) +{ + if(F->Locking && F->Handle) + { + fclose(F->Handle); + } + F->Handle = fopen(F->Path, "w"); + TryLock(F); + return F->Handle; +} + // TODO(matt): Consider enforcing an order of these blocks, basically putting the easy-to-skip ones first... void * InitBlock(block_id ID) { db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location; ++Header->BlockCount; - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); @@ -5419,9 +5486,10 @@ IsSymlink(char *Filepath) return Result; } -void +watch_file * PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extension, watch_type Type, project *Project, asset *Asset) { + watch_file *Result = 0; bool Required = TRUE; for(int i = 0; i < Handle->Files.ItemCount; ++i) { @@ -5432,14 +5500,16 @@ PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extens { if(This->Extension == Extension) { + Result = This; Required = FALSE; break; } } else if(StringsMatch(This->Path, Filepath)) { - Required = FALSE; + Result = This; EraseCurrentStringFromBook(&WatchHandles.Paths); + Required = FALSE; break; } } @@ -5459,7 +5529,9 @@ PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extens New->Type = Type; New->Project = Project; New->Asset = Asset; + Result = New; } + return Result; } bool @@ -5535,9 +5607,10 @@ GetNearestExistingPath(string Path) return Result; } -void +watch_file * PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset) { + watch_file *Result = 0; // NOTE(matt): This function influences RemoveAndFreeAllButFirstWatchFile(). If we change to write different strings into // the W->Paths memory_book, we must reflect that change over to RemoveAndFreeAllButFirstWatchFile() @@ -5675,7 +5748,8 @@ PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *P //PrintWatchHandles(); } - PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset); + Result = PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset); + return Result; } bool @@ -5795,6 +5869,17 @@ UpdateNeighbourhoodPointers(neighbourhood *N, file_signposts *S) N->Next = S->Next.Ptr; } +void +WriteEntireDatabase(neighbourhood *N) +{ + // NOTE(matt): This may suffice when the only changes are to existing fixed-size variables, + // e.g. ProjectsBlock->GlobalSearchDir + OpenFileForWriting(&DB.Metadata.File); + fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); + CycleSignpostedFile(&DB.Metadata); + UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); +} + int UpdateAssetInDB(asset *Asset) { @@ -5854,7 +5939,7 @@ UpdateAssetInDB(asset *Asset) FreeString(&Message); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); @@ -5865,7 +5950,7 @@ UpdateAssetInDB(asset *Asset) else { // Append new asset, not bothering to insertion sort because there likely won't be many - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); char *InsertionPoint = LocateEndOfAssetsBlock(AssetsBlock); AssetIndexInDB = AssetsBlock->Count; @@ -5994,7 +6079,7 @@ UpdateAsset(asset *Asset, bool Defer) { AssetIndexInDB = UpdateAssetInDB(Asset); } - FreeFile(&File); + FreeFile(&File, NA); return AssetIndexInDB; } @@ -6068,7 +6153,7 @@ PlaceAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated, printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path); } - FreeFile(&File); + FreeFile(&File, NA); return This; } @@ -7601,7 +7686,7 @@ GenerateTopicColours(neighbourhood *N, string Topic) printf(" Freed Topics (%ld)\n", Topics.Buffer.Size); #endif - fclose(Topics.Handle); + CloseFile(&Topics, NA); asset *Asset = GetPlaceInBook(&Assets, ASSET_CSS_TOPICS); if(Asset->Known) @@ -7623,7 +7708,7 @@ GenerateTopicColours(neighbourhood *N, string Topic) } } } - FreeFile(&Topics); + FreeFile(&Topics, NA); } return Result; } @@ -9115,7 +9200,7 @@ void ExamineDB(void) { DB.Metadata.File.Buffer.ID = BID_DATABASE; - DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL); + DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL, TRUE); ReadFileIntoBuffer(&DB.Metadata.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? if(DB.Metadata.File.Buffer.Location) @@ -9146,7 +9231,7 @@ ExamineDB(void) { fprintf(stderr, "Unable to open database file %s: %s\n", DB.Metadata.File.Path, strerror(errno)); } - FreeFile(&DB.Metadata.File); + FreeFile(&DB.Metadata.File, TRUE); } #define HMMLCleanup() \ @@ -9323,7 +9408,7 @@ DeleteSearchPageFromFilesystem(string BaseDir, string SearchLocation, string Pro remove(DB.File.Path); // TODO(matt): Consider the correctness of this. - FreeFile(&DB.File); + FreeFile(&DB.File, NA); char *SearchDirectory = ConstructDirectoryPath(&BaseDir, &SearchLocation, 0); remove(SearchDirectory); @@ -11866,7 +11951,7 @@ InitIndexFile(project *P) DB.File.Handle = fopen(DB.File.Path, "w"); fprintf(DB.File.Handle, "---\n"); - fclose(DB.File.Handle); + CloseFile(&DB.File, NA); ReadFileIntoBuffer(&DB.File); } } @@ -12243,10 +12328,7 @@ RenumberEntries(neighbourhood *N, db_header_project *P, int64_t IndexOfFirstEntr Cursor += fwrite(Cursor, 1, DB.File.Buffer.Size - (Cursor - DB.File.Buffer.Location), DB.File.Handle); CycleFile(&DB.File); - if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { return; } - fwrite(DB.Metadata.File.Buffer.Location, 1, DB.Metadata.File.Buffer.Size, DB.Metadata.File.Handle); - CycleSignpostedFile(&DB.Metadata); - UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); + WriteEntireDatabase(N); } db_entry * @@ -12328,7 +12410,7 @@ InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTempl fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle); fwrite(CollationBuffers->SearchEntry.Location, N->This->Size, 1, DB.File.Handle); fwrite(DB.File.Buffer.Location + EntryInsertionEnd, DB.File.Buffer.Size - EntryInsertionEnd, 1, DB.File.Handle); - fclose(DB.File.Handle); + CloseFile(&DB.File, NA); if(N->This->Size == EntryInsertionEnd - EntryInsertionStart) { @@ -12350,7 +12432,7 @@ InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTempl Ptr += sizeof(*N->Project) + sizeof(db_entry) * N->ThisIndex; uint64_t BytesIntoFile = Ptr - DB.Metadata.File.Buffer.Location; - if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { return 0; } + if(!(OpenFileForWriting(&DB.Metadata.File))) { return 0; } fwrite(DB.Metadata.File.Buffer.Location, BytesIntoFile, 1, DB.Metadata.File.Handle); SetFileEditPosition(&DB.Metadata); @@ -12401,7 +12483,7 @@ WritePastAssetsHeader(void) DB.Metadata.File.Buffer.Ptr = DB.Metadata.Signposts.AssetsBlock.Ptr; DB.Metadata.File.Buffer.Ptr += sizeof(db_block_assets); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Ptr - DB.Metadata.File.Buffer.Location, 1, DB.Metadata.File.Handle); } @@ -13197,7 +13279,7 @@ InsertNeighbourLink(db_header_project *P, db_entry *From, db_entry *To, enum8(li if(To) { DeclaimBuffer(&ToPlayerURL); } DeclaimBuffer(&Link); - fclose(HTML.Handle); + CloseFile(&HTML, NA); HTML.Handle = 0; } } @@ -13205,7 +13287,7 @@ InsertNeighbourLink(db_header_project *P, db_entry *From, db_entry *To, enum8(li { Result = RC_ERROR_FILE; } - FreeFile(&HTML); + FreeFile(&HTML, NA); MEM_TEST_END("InsertNeighbourLink()"); return Result; } @@ -13236,14 +13318,14 @@ DeleteNeighbourLinks(neighbourhood *N) ReadPlayerPageIntoBuffer(&HTML, Wrap0i(N->Project->BaseDir), Wrap0i(N->Project->PlayerLocation), Wrap0i(Entry->OutputLocation)); if(HTML.Buffer.Location) { - if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeFile(&HTML); return RC_ERROR_FILE; }; + if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeFile(&HTML, NA); return RC_ERROR_FILE; }; fwrite(HTML.Buffer.Location, Entry->LinkOffsets.PrevStart, 1, HTML.Handle); fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd, Entry->LinkOffsets.NextStart, 1, HTML.Handle); fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd, HTML.Buffer.Size - (Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd), 1, HTML.Handle); - fclose(HTML.Handle); + CloseFile(&HTML, NA); Entry->LinkOffsets.PrevEnd = 0; Entry->LinkOffsets.NextEnd = 0; if(N->Prev && N->PrevIndex >= 0) @@ -13262,7 +13344,7 @@ DeleteNeighbourLinks(neighbourhood *N) } } - FreeFile(&HTML); + FreeFile(&HTML, NA); return RC_SUCCESS; } @@ -13331,7 +13413,7 @@ MarkNextAsFirst(neighbourhood *N) N->Next->LinkOffsets.PrevEnd = Link.Ptr - Link.Location; DeclaimBuffer(&Link); - fclose(HTML.Handle); + CloseFile(&HTML, NA); } else { @@ -13339,7 +13421,7 @@ MarkNextAsFirst(neighbourhood *N) N->Next->LinkOffsets = Blank; } - FreeFile(&HTML); + FreeFile(&HTML, NA); } void @@ -13369,7 +13451,7 @@ MarkPrevAsFinal(neighbourhood *N) N->Prev->LinkOffsets.NextEnd = Link.Ptr - Link.Location; DeclaimBuffer(&Link); - fclose(HTML.Handle); + CloseFile(&HTML, NA); } else { @@ -13377,7 +13459,7 @@ MarkPrevAsFinal(neighbourhood *N) N->Prev->LinkOffsets = Blank; } - FreeFile(&HTML); + FreeFile(&HTML, NA); } void @@ -13534,7 +13616,7 @@ DeleteFromDB(neighbourhood *N, string BaseFilename, db_entry *Deceased) } N->Project->EntryCount = NewEntryCount; - if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; } + if(!(OpenFileForWriting(&DB.Metadata.File))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; } char *Ptr = (char *)N->This; uint64_t BytesIntoFile = Ptr - DB.Metadata.File.Buffer.Location; @@ -14560,7 +14642,7 @@ SetCurrentProject(project *P, neighbourhood *N) { if(CurrentProject != P) { - FreeFile(&DB.File); + FreeFile(&DB.File, NA); if(CurrentProject) { @@ -14608,7 +14690,6 @@ RecheckPrivacyRecursively(project *P, neighbourhood *N, buffers *CollationBuffer DeleteStaleAssets(); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); } - } for(int i = 0; i < P->Child.ItemCount; ++i) @@ -14723,7 +14804,7 @@ UpgradeDB(int OriginalDBVersion) ClearCopyStringNoFormat(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir); ++DB.Header.BlockCount; - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.File.Handle); DB.Metadata.Signposts.ProjectsBlock.Byte = ftell(DB.Metadata.File.Handle); @@ -14829,7 +14910,7 @@ DeleteDeadDBEntries(neighbourhood *N, bool *Modified) Free(NewSearchDirectory); - FreeFile(&DB.File); + FreeFile(&DB.File, NA); DB.File.Path = ConstructIndexFilePath(CurrentProject->BaseDir, CurrentProject->SearchLocation, CurrentProject->ID); ReadFileIntoBuffer(&DB.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? @@ -14847,7 +14928,7 @@ DeleteDeadDBEntries(neighbourhood *N, bool *Modified) if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir))) { ClearCopyStringNoFormat(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToEnd(&DB.Metadata.File, 0); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); @@ -14857,7 +14938,7 @@ DeleteDeadDBEntries(neighbourhood *N, bool *Modified) if(StringsDiffer(CurrentProject->BaseURL, Wrap0i(N->Project->BaseURL))) { ClearCopyStringNoFormat(N->Project->BaseURL, sizeof(N->Project->BaseURL), CurrentProject->BaseURL); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToEnd(&DB.Metadata.File, 0); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); @@ -14866,7 +14947,7 @@ DeleteDeadDBEntries(neighbourhood *N, bool *Modified) if(HasNewPlayerLocation || HasNewSearchLocation) { - if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; } + if(!(OpenFileForWriting(&DB.Metadata.File))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; } ClearCopyStringNoFormat(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir); ClearCopyStringNoFormat(N->Project->BaseURL, sizeof(N->Project->BaseURL), CurrentProject->BaseURL); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); @@ -14942,7 +15023,7 @@ SyncDBWithInput(neighbourhood *N, buffers *CollationBuffers, template *BespokeTe if(StringsDiffer(CurrentProject->Title, Wrap0i(N->Project->Title))) { ClearCopyStringNoFormat(N->Project->Title, sizeof(N->Project->Title), CurrentProject->Title); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToEnd(&DB.Metadata.File, 0); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); @@ -14952,7 +15033,7 @@ SyncDBWithInput(neighbourhood *N, buffers *CollationBuffers, template *BespokeTe if(StringsDiffer(CurrentProject->Theme, Wrap0i(N->Project->Theme))) { ClearCopyStringNoFormat(N->Project->Theme, sizeof(N->Project->Theme), CurrentProject->Theme); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToEnd(&DB.Metadata.File, 0); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); @@ -14962,7 +15043,7 @@ SyncDBWithInput(neighbourhood *N, buffers *CollationBuffers, template *BespokeTe if(StringsDiffer(CurrentProject->Numbering.Unit, Wrap0i(N->Project->Unit))) { ClearCopyStringNoFormat(N->Project->Unit, sizeof(N->Project->Unit), CurrentProject->Numbering.Unit); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToEnd(&DB.Metadata.File, 0); CycleSignpostedFile(&DB.Metadata); UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); @@ -15082,16 +15163,6 @@ InitAccumulator(project_generations *G) return Result; } -void -WriteEntireDatabase() -{ - // NOTE(matt): This may suffice when the only changes are to existing fixed-size variables, - // e.g. ProjectsBlock->GlobalSearchDir - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); - fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); - fclose(DB.Metadata.File.Handle); -} - rc InitDB(void) { @@ -15101,170 +15172,176 @@ InitDB(void) // need a separate InitIndex() function that we can call when looping over the projects after ParseConfig() DB.Metadata.File.Buffer.ID = BID_DATABASE; - DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL); - ReadFileIntoBuffer(&DB.Metadata.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail? - - if(DB.Metadata.File.Buffer.Location) + DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL, TRUE); + if(ReadFileIntoBuffer(&DB.Metadata.File) == RC_ERROR_FILE_LOCKED) // NOTE(matt): Could we actually catch errors (permissions?) here and bail? { - // TODO(matt): Handle this gracefully (it'll be an invalid file) - Assert(DB.Metadata.File.Buffer.Size >= sizeof(DB.Header)); - uint32_t OriginalDBVersion = 0; - uint32_t FirstInt = *(uint32_t *)DB.Metadata.File.Buffer.Location; - if(FirstInt != FOURCC("CNRA")) - { - // TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than - // trying to upgrade a file that isn't even valid - // TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files - // Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition - OriginalDBVersion = FirstInt; - } - else - { - OriginalDBVersion = *(uint32_t *)(DB.Metadata.File.Buffer.Location + sizeof(DB.Header.HexSignature)); - } - - if(OriginalDBVersion < CINERA_DB_VERSION) - { - if(CINERA_DB_VERSION == 6) - { - fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]); - FreeConfig(Config); - _exit(RC_ERROR_FATAL); - } - if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { FreeConfig(Config); return RC_NOOP; } - } - else if(OriginalDBVersion > CINERA_DB_VERSION) - { - fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]); - FreeConfig(Config); - _exit(RC_ERROR_FATAL); - } - - db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location; - Header->CurrentAppVersion = CINERA_APP_VERSION; - Header->CurrentHMMLVersion.Major = hmml_version.Major; - Header->CurrentHMMLVersion.Minor = hmml_version.Minor; - Header->CurrentHMMLVersion.Patch = hmml_version.Patch; - - buffer *B = &DB.Metadata.File.Buffer; - B->Ptr = B->Location + sizeof(*Header); - - for(int BlockIndex = 0; BlockIndex < Header->BlockCount; ++BlockIndex) - { - uint32_t BlockID = *(uint32_t *)B->Ptr; - if(BlockID == FOURCC("PROJ")) - { - DB.Metadata.Signposts.ProjectsBlock.Ptr = B->Ptr; - B->Ptr = SkipBlock(B->Ptr); - } - else if(BlockID == FOURCC("ASET")) - { - DB.Metadata.Signposts.AssetsBlock.Ptr = B->Ptr; - DB.Metadata.File.Buffer.Ptr = SkipBlock(B->Ptr); - } - else - { - fprintf(stderr, "%sMalformed database%s: %s\n", - ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.File.Path); - } - } - - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); - if(DB.Metadata.File.Handle) - { - fwrite(B->Location, B->Size, 1, DB.Metadata.File.Handle); - SetFileEditPosition(&DB.Metadata); - CycleSignpostedFile(&DB.Metadata); - } - else - { - // TODO(matt): Handle unopenable database files - PrintC(CS_RED, "Could not open database file: "); - PrintC(CS_MAGENTA, DB.Metadata.File.Path); - fprintf(stderr, "\n"); - _exit(0); - } + ConfigErrorLockedDBLocation(0, 0, &Config->DatabaseLocation); + CloseFile(&DB.Metadata.File, TRUE); + Result = RC_ERROR_FILE_LOCKED; } else { - // NOTE(matt): Initialising new db_header - DB.Header.HexSignature = FOURCC("CNRA"); - DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION; - DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION; - DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major; - DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor; - DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch; - DB.Header.BlockCount = 0; - - DB.ProjectsBlock.BlockID = FOURCC("PROJ"); - ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchDir, sizeof(DB.ProjectsBlock.GlobalSearchDir), Config->GlobalSearchDir); - ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchURL, sizeof(DB.ProjectsBlock.GlobalSearchURL), Config->GlobalSearchURL); - DB.ProjectsBlock.Count = 0; - ++DB.Header.BlockCount; - - DB.AssetsBlock.BlockID = FOURCC("ASET"); - DB.AssetsBlock.Count = 0; - ClearCopyStringNoFormat(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir); - ClearCopyStringNoFormat(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL); - ClearCopyStringNoFormat(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir); - ClearCopyStringNoFormat(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir); - ClearCopyStringNoFormat(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir); - ++DB.Header.BlockCount; - - char *DatabaseLocation0 = MakeString0("l", &Config->DatabaseLocation); - StripComponentFromPath0(DatabaseLocation0); - DIR *OutputDirectoryHandle = opendir(DatabaseLocation0); - if(!OutputDirectoryHandle) + if(DB.Metadata.File.Buffer.Location) { - if(!MakeDir(Wrap0(DatabaseLocation0))) + // TODO(matt): Handle this gracefully (it'll be an invalid file) + Assert(DB.Metadata.File.Buffer.Size >= sizeof(DB.Header)); + uint32_t OriginalDBVersion = 0; + uint32_t FirstInt = *(uint32_t *)DB.Metadata.File.Buffer.Location; + if(FirstInt != FOURCC("CNRA")) { - LogError(LOG_ERROR, "Unable to create directory %.*s: %s", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno)); - fprintf(stderr, "Unable to create directory %.*s: %s\n", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno)); - Free(DatabaseLocation0); - Result = RC_ERROR_DIRECTORY; - }; - } - else - { - closedir(OutputDirectoryHandle); - OutputDirectoryHandle = 0; - } - Free(DatabaseLocation0); - - if(Result == RC_SUCCESS) - { - - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); - if(DB.Metadata.File.Handle) + // TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than + // trying to upgrade a file that isn't even valid + // TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files + // Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition + OriginalDBVersion = FirstInt; + } + else { - fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.File.Handle); + OriginalDBVersion = *(uint32_t *)(DB.Metadata.File.Buffer.Location + sizeof(DB.Header.HexSignature)); + } - DB.Metadata.Signposts.ProjectsBlock.Byte = ftell(DB.Metadata.File.Handle); - fwrite(&DB.ProjectsBlock, sizeof(DB.ProjectsBlock), 1, DB.Metadata.File.Handle); + if(OriginalDBVersion < CINERA_DB_VERSION) + { + if(CINERA_DB_VERSION == 6) + { + fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]); + FreeConfig(Config); + _exit(RC_ERROR_FATAL); + } + if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { FreeConfig(Config); return RC_NOOP; } + } + else if(OriginalDBVersion > CINERA_DB_VERSION) + { + fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]); + FreeConfig(Config); + _exit(RC_ERROR_FATAL); + } - DB.Metadata.Signposts.AssetsBlock.Byte = ftell(DB.Metadata.File.Handle); - fwrite(&DB.AssetsBlock, sizeof(DB.AssetsBlock), 1, DB.Metadata.File.Handle); + db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location; + Header->CurrentAppVersion = CINERA_APP_VERSION; + Header->CurrentHMMLVersion.Major = hmml_version.Major; + Header->CurrentHMMLVersion.Minor = hmml_version.Minor; + Header->CurrentHMMLVersion.Patch = hmml_version.Patch; - fclose(DB.Metadata.File.Handle); - ReadFileIntoBuffer(&DB.Metadata.File); - DB.Metadata.Signposts.ProjectsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.ProjectsBlock.Byte; - DB.Metadata.Signposts.AssetsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.AssetsBlock.Byte; + buffer *B = &DB.Metadata.File.Buffer; + B->Ptr = B->Location + sizeof(*Header); + + for(int BlockIndex = 0; BlockIndex < Header->BlockCount; ++BlockIndex) + { + uint32_t BlockID = *(uint32_t *)B->Ptr; + if(BlockID == FOURCC("PROJ")) + { + DB.Metadata.Signposts.ProjectsBlock.Ptr = B->Ptr; + B->Ptr = SkipBlock(B->Ptr); + } + else if(BlockID == FOURCC("ASET")) + { + DB.Metadata.Signposts.AssetsBlock.Ptr = B->Ptr; + DB.Metadata.File.Buffer.Ptr = SkipBlock(B->Ptr); + } + else + { + fprintf(stderr, "%sMalformed database%s: %s\n", + ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.File.Path); + } + } + + if(OpenFileForWriting(&DB.Metadata.File)) + { + fwrite(B->Location, B->Size, 1, DB.Metadata.File.Handle); + SetFileEditPosition(&DB.Metadata); + CycleSignpostedFile(&DB.Metadata); + DB.Ready = TRUE; } else { // TODO(matt): Handle unopenable database files PrintC(CS_RED, "Could not open database file: "); - PrintC(CS_MAGENTA_BOLD, DB.Metadata.File.Path); + PrintC(CS_MAGENTA, DB.Metadata.File.Path); fprintf(stderr, "\n"); _exit(0); } + } + else + { + // NOTE(matt): Initialising new db_header + DB.Header.HexSignature = FOURCC("CNRA"); + DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION; + DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION; + DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major; + DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor; + DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch; + DB.Header.BlockCount = 0; + + DB.ProjectsBlock.BlockID = FOURCC("PROJ"); + ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchDir, sizeof(DB.ProjectsBlock.GlobalSearchDir), Config->GlobalSearchDir); + ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchURL, sizeof(DB.ProjectsBlock.GlobalSearchURL), Config->GlobalSearchURL); + DB.ProjectsBlock.Count = 0; + ++DB.Header.BlockCount; + + DB.AssetsBlock.BlockID = FOURCC("ASET"); + DB.AssetsBlock.Count = 0; + ClearCopyStringNoFormat(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir); + ClearCopyStringNoFormat(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL); + ClearCopyStringNoFormat(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir); + ClearCopyStringNoFormat(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir); + ClearCopyStringNoFormat(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir); + ++DB.Header.BlockCount; + + char *DatabaseLocation0 = MakeString0("l", &Config->DatabaseLocation); + StripComponentFromPath0(DatabaseLocation0); + DIR *OutputDirectoryHandle = opendir(DatabaseLocation0); + if(!OutputDirectoryHandle) + { + if(!MakeDir(Wrap0(DatabaseLocation0))) + { + LogError(LOG_ERROR, "Unable to create directory %.*s: %s", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno)); + fprintf(stderr, "Unable to create directory %.*s: %s\n", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno)); + Free(DatabaseLocation0); + Result = RC_ERROR_DIRECTORY; + }; + } + else + { + closedir(OutputDirectoryHandle); + OutputDirectoryHandle = 0; + } + Free(DatabaseLocation0); + + if(Result == RC_SUCCESS) + { + if(OpenFileForWriting(&DB.Metadata.File)) + { + fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.File.Handle); + + DB.Metadata.Signposts.ProjectsBlock.Byte = ftell(DB.Metadata.File.Handle); + fwrite(&DB.ProjectsBlock, sizeof(DB.ProjectsBlock), 1, DB.Metadata.File.Handle); + + DB.Metadata.Signposts.AssetsBlock.Byte = ftell(DB.Metadata.File.Handle); + fwrite(&DB.AssetsBlock, sizeof(DB.AssetsBlock), 1, DB.Metadata.File.Handle); + + CloseFile(&DB.Metadata.File, FALSE); + ReadFileIntoBuffer(&DB.Metadata.File); + DB.Metadata.Signposts.ProjectsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.ProjectsBlock.Byte; + DB.Metadata.Signposts.AssetsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.AssetsBlock.Byte; + DB.Ready = TRUE; + } + else + { + // TODO(matt): Handle unopenable database files + PrintC(CS_RED, "Could not open database file: "); + PrintC(CS_MAGENTA_BOLD, DB.Metadata.File.Path); + fprintf(stderr, "\n"); + _exit(0); + } #if 0 - DB.File.Handle = fopen(DB.File.Path, "w"); - fprintf(DB.File.Handle, "---\n"); - fclose(DB.File.Handle); - ReadFileIntoBuffer(&DB.File); + DB.File.Handle = fopen(DB.File.Path, "w"); + fprintf(DB.File.Handle, "---\n"); + CloseFile(&DB.File, NA); + ReadFileIntoBuffer(&DB.File); #endif + } } } return Result; @@ -15407,7 +15484,7 @@ InsertProjectIntoDB_TopLevel(project_generations *G, db_block_projects **Block, { uint64_t Byte = 0; - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Block); db_block_projects NewBlock = **Block; @@ -15431,7 +15508,7 @@ InsertProjectIntoDB(project_generations *G, db_header_project **Parent, db_heade { uint64_t Byte = 0; - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent); db_header_project NewParent = **Parent; @@ -15633,7 +15710,7 @@ DeleteProject_TopLevel(db_block_projects **Parent, db_header_project **Child, pr uint64_t PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location; uint64_t CPos = (char *)*Child - DB.Metadata.File.Buffer.Location; - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); uint64_t Byte = 0; WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent); @@ -15663,7 +15740,7 @@ DeleteProject(db_header_project **Parent, db_header_project **Child, project_gen uint64_t CPos = (char *)*Child - DB.Metadata.File.Buffer.Location; //db_project_index Index = GetCurrentProjectIndex(G); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); uint64_t Byte = 0; WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent); @@ -15723,7 +15800,7 @@ ReorganiseProjectsInterior(project_generations *G, project *CChild, uint64_t Chi project_generations ThisAcc = InitAccumulator(G); AccumulateProjectIndices(&ThisAcc, This); - DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); + OpenFileForWriting(&DB.Metadata.File); uint64_t Byte = 0; WriteFromByteToPointer(&DB.Metadata.File, &Byte, SChild); @@ -16036,13 +16113,13 @@ SyncGlobalPagesWithInput(neighbourhood *N, buffers *CollationBuffers) if(StringsDiffer(StoredGlobalSearchDir, Config->GlobalSearchDir)) { ClearCopyStringNoFormat(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir), Config->GlobalSearchDir); - WriteEntireDatabase(); + WriteEntireDatabase(N); } if(StringsDiffer(StoredGlobalSearchURL, Config->GlobalSearchURL)) { ClearCopyStringNoFormat(ProjectsBlock->GlobalSearchURL, sizeof(ProjectsBlock->GlobalSearchURL), Config->GlobalSearchURL); - WriteEntireDatabase(); + WriteEntireDatabase(N); } if(!ProjectsBlock->GlobalSearchDir[0]) @@ -16350,6 +16427,15 @@ RemoveAndFreeWatchHandles(watch_handles *W) { watch_handle *This = GetPlaceInBook(&W->Handles, i); inotify_rm_watch(inotifyInstance, This->Descriptor); + for(int j = 0; j < This->Files.ItemCount; ++j) + { + watch_file *File = GetPlaceInBook(&This->Files, j); + if(File->Handle) + { + fclose(File->Handle); + File->Handle = 0; + } + } FreeBook(&This->Files); } FreeAndReinitialiseBook(&W->Handles); @@ -16359,8 +16445,9 @@ RemoveAndFreeWatchHandles(watch_handles *W) void DiscardAllAndFreeConfig(void) { - FreeSignpostedFile(&DB.Metadata); // NOTE(matt): This seems fine - FreeFile(&DB.File); // NOTE(matt): This seems fine + FreeSignpostedFile(&DB.Metadata, TRUE); // NOTE(matt): This seems fine + FreeFile(&DB.File, NA); // NOTE(matt): This seems fine + DB.Ready = FALSE; FreeAssets(&Assets); // NOTE(matt): This seems fine RemoveAndFreeWatchHandles(&WatchHandles); CurrentProject = 0; // NOTE(matt): This is fine @@ -16573,24 +16660,30 @@ MonitorFilesystem(neighbourhood *N, buffers *CollationBuffers, template *Bespoke { case WT_HMML: { - SetCurrentProject(WatchFile->Project, N); - string BaseFilename = GetBaseFilename(Wrap0(Event->name), WatchFile->Extension); - if(Event->mask & (IN_DELETE | IN_MOVED_FROM)) + if(DB.Ready) { - Deleted |= (DeleteEntry(N, BaseFilename) == RC_SUCCESS); - } - else if(Event->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)) - { - Inserted |= (InsertEntry(N, CollationBuffers, BespokeTemplate, BaseFilename, 0) == RC_SUCCESS); + SetCurrentProject(WatchFile->Project, N); + string BaseFilename = GetBaseFilename(Wrap0(Event->name), WatchFile->Extension); + if(Event->mask & (IN_DELETE | IN_MOVED_FROM)) + { + Deleted |= (DeleteEntry(N, BaseFilename) == RC_SUCCESS); + } + else if(Event->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)) + { + Inserted |= (InsertEntry(N, CollationBuffers, BespokeTemplate, BaseFilename, 0) == RC_SUCCESS); + } } } break; case WT_ASSET: { - UpdateAsset(WatchFile->Asset, FALSE); - UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); + if(DB.Ready) + { + UpdateAsset(WatchFile->Asset, FALSE); + UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts); #if DEBUG_LANDMARKS - UpdatedAsset = TRUE; + UpdatedAsset = TRUE; #endif + } } break; case WT_CONFIG: { @@ -16749,7 +16842,6 @@ main(int ArgC, char **Args) PushWatchHandle(ConfigPathL, EXT_NULL, WT_CONFIG, 0, 0); Config = ParseConfig(ConfigPathL, &TokensList); - rc Succeeding = RC_SUCCESS; if(Config) { if(Mode & MODE_EXAMINE) @@ -16766,7 +16858,7 @@ main(int ArgC, char **Args) else { /* */ MEM_TEST_MID("main()"); - /* +MEM */ Succeeding = InitAll(&Neighbourhood, &CollationBuffers, &BespokeTemplate); + /* +MEM */ InitAll(&Neighbourhood, &CollationBuffers, &BespokeTemplate); /* */ MEM_TEST_MID("main()"); } } @@ -16782,39 +16874,35 @@ main(int ArgC, char **Args) } } - if(Succeeding == RC_SUCCESS) + if(inotifyInstance != -1) { - if(inotifyInstance != -1) + while(GlobalRunning && MonitorFilesystem(&Neighbourhood, &CollationBuffers, &BespokeTemplate, ConfigPathL, &TokensList) != RC_ARENA_FULL) { - while(GlobalRunning && MonitorFilesystem(&Neighbourhood, &CollationBuffers, &BespokeTemplate, ConfigPathL, &TokensList) != RC_ARENA_FULL) + // TODO(matt): Refetch the quotes and rebuild player pages if needed + // + // Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). But here we still don't even know + // who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe + // postpone this for now, but we will certainly need this to happen + // + // The most ideal solution is possibly that we store quote numbers in the Metadata->Entry, listen for and handle a + // REST PUT request from insobot when a quote changes (unless we're supposed to poll insobot for them?), and rebuild + // the player page(s) accordingly. + // + if(!(Mode & MODE_DRYRUN) && DB.Ready && Config && Config->RespectingPrivacy && time(0) - LastPrivacyCheck > Config->PrivacyCheckInterval) { - // TODO(matt): Refetch the quotes and rebuild player pages if needed - // - // Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). But here we still don't even know - // who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe - // postpone this for now, but we will certainly need this to happen - // - // The most ideal solution is possibly that we store quote numbers in the Metadata->Entry, listen for and handle a - // REST PUT request from insobot when a quote changes (unless we're supposed to poll insobot for them?), and rebuild - // the player page(s) accordingly. - // - if(!(Mode & MODE_DRYRUN) && Config && Config->RespectingPrivacy && time(0) - LastPrivacyCheck > Config->PrivacyCheckInterval) - { - RecheckPrivacy(&Neighbourhood, &CollationBuffers, &BespokeTemplate); - } - sleep(GLOBAL_UPDATE_INTERVAL); + RecheckPrivacy(&Neighbourhood, &CollationBuffers, &BespokeTemplate); } + sleep(GLOBAL_UPDATE_INTERVAL); } - else if(GlobalRunning) - { - string inotifyErrorL = Wrap0(strerror(inotifyError)); - SystemError(0, 0, S_ERROR, "inotify initialisation failed with message: ", &inotifyErrorL); - } - - DiscardAllAndFreeConfig(); - RemoveAndFreeWatchHandles(&WatchHandles); - MEM_TEST_END("main()"); } + else if(GlobalRunning) + { + string inotifyErrorL = Wrap0(strerror(inotifyError)); + SystemError(0, 0, S_ERROR, "inotify initialisation failed with message: ", &inotifyErrorL); + } + + DiscardAllAndFreeConfig(); + MEM_TEST_END("main()"); } Exit(); } diff --git a/cinera/cinera_config.c b/cinera/cinera_config.c index 456bcdf..9a60320 100644 --- a/cinera/cinera_config.c +++ b/cinera/cinera_config.c @@ -8,12 +8,6 @@ exit // config // -#include -#include -#include -#include -#include - #define ArgCountPointer(...) (sizeof((void *[]){__VA_ARGS__})/sizeof(void *)) #define DigitsInInt(I) DigitsInInt_(I, sizeof(*I)) @@ -348,6 +342,15 @@ PushTokens(memory_book *TokensList) return This; } +void +PopTokens(memory_book *TokensList) +{ + --TokensList->ItemCount; + tokens *This = GetPlaceInBook(TokensList, TokensList->ItemCount); + FreeBook(&This->Token); + This->CurrentLine = 0; +} + void FreeTokensList(memory_book *TokensList) { @@ -357,7 +360,8 @@ FreeTokensList(memory_book *TokensList) This->CurrentIndex = 0; This->CurrentLine = 0; FreeBook(&This->Token); - FreeFile(&This->File); + FreeFileBufferAndPath(&This->File); // NOTE(matt): Not closing file because the next time we touch this handle is in + // RemoveAndFreeWatchHandles() where we will close it } TokensList->ItemCount = 0; } @@ -588,194 +592,204 @@ typedef struct } config; char *ExpandPath(string Path, string *RelativeToFile); // NOTE(matt): Forward declared. Consider reorganising the code? -void PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset); // NOTE(matt): Forward declared. Consider reorganising the code? +watch_file *PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset); // NOTE(matt): Forward declared. Consider reorganising the code? tokens * -Tokenise(memory_book *TokensList, string Path) +Tokenise(memory_book *TokensList, string Path, string *Filename, uint64_t LineNumber) { tokens *Result = 0; - char *Path0 = MakeString0("l", &Path); - FILE *Handle = 0; - PushWatchHandle(Path, EXT_NULL, WT_CONFIG, 0, 0); - if((Handle = fopen(Path0, "r"))) + watch_file *WatchFile = PushWatchHandle(Path, EXT_NULL, WT_CONFIG, 0, 0); + + Result = PushTokens(TokensList); + Result->File = InitFile(0, &Path, EXT_NULL, TRUE); + switch(ReadFileIntoBuffer(&Result->File)) { - fclose(Handle); - - Result = PushTokens(TokensList); - Result->File = InitFile(0, &Path, EXT_NULL); - ReadFileIntoBuffer(&Result->File); - buffer *B = &Result->File.Buffer; - SkipWhitespace(Result, B); - while(B->Ptr - B->Location < B->Size) - { - token T = {}; - uint64_t Advancement = 0; - - if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_SINGLE], B)) + case RC_ERROR_FILE_LOCKED: { - T.Type = TOKEN_COMMENT; - B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_SINGLE]); - SkipWhitespace(Result, B); - T.Content.Base = B->Ptr; - while(B->Ptr && *B->Ptr != '\n') - { - ++T.Content.Length; - ++B->Ptr; - } - if(*B->Ptr == '\n') - { - ++Result->CurrentLine; - } - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_OPEN], B)) + ConfigErrorLockedConfigLocation(Filename, LineNumber, &Path); + FreeFile(&Result->File, TRUE); + PopTokens(TokensList); + Result = 0; + } break; + case RC_ERROR_FILE: { - uint64_t CommentDepth = 1; - T.Type = TOKEN_COMMENT; - B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_OPEN]); + ConfigErrorUnopenableConfigLocation(Filename, LineNumber, &Path); + FreeFile(&Result->File, TRUE); + PopTokens(TokensList); + Result = 0; + } break; + default: + { + WatchFile->Handle = Result->File.Handle; + + buffer *B = &Result->File.Buffer; SkipWhitespace(Result, B); - T.Content.Base = B->Ptr; - while(B->Ptr - B->Location < B->Size && CommentDepth) + while(B->Ptr - B->Location < B->Size) { - if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) + token T = {}; + uint64_t Advancement = 0; + + if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_SINGLE], B)) { - --CommentDepth; - B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); - } - else if(B->Ptr - B->Location < B->Size && *B->Ptr == '\n') - { - ++Result->CurrentLine; - ++B->Ptr; + T.Type = TOKEN_COMMENT; + B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_SINGLE]); + SkipWhitespace(Result, B); + T.Content.Base = B->Ptr; + while(B->Ptr && *B->Ptr != '\n') + { + ++T.Content.Length; + ++B->Ptr; + } + if(*B->Ptr == '\n') + { + ++Result->CurrentLine; + } + Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_OPEN], B)) { - ++CommentDepth; + uint64_t CommentDepth = 1; + T.Type = TOKEN_COMMENT; B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_OPEN]); + SkipWhitespace(Result, B); + T.Content.Base = B->Ptr; + while(B->Ptr - B->Location < B->Size && CommentDepth) + { + if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) + { + --CommentDepth; + B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); + } + else if(B->Ptr - B->Location < B->Size && *B->Ptr == '\n') + { + ++Result->CurrentLine; + ++B->Ptr; + } + else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_OPEN], B)) + { + ++CommentDepth; + B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_OPEN]); + } + else + { + ++B->Ptr; + } + } + T.Content.Length = B->Ptr - T.Content.Base; + Advancement = 0;//StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); + } + else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) + { + Advancement = 2; + T.Type = TOKEN_NULL; + string Char = { .Base = B->Ptr, .Length = 2 }; + string Filepath = Wrap0(Result->File.Path); + ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Mismatched closing multiline comment marker: ", &Char); + } + else if(!StringsDifferS(TokenStrings[TOKEN_DOUBLEQUOTE], B)) + { + T.Type = TOKEN_STRING; + ++B->Ptr; + T.Content.Base = B->Ptr; + while(B->Ptr - B->Location < B->Size && *B->Ptr != '"') + { + if(*B->Ptr == '\\') + { + ++T.Content.Length; + ++B->Ptr; + } + ++T.Content.Length; + ++B->Ptr; + } + Advancement = 1; + } + else if(!StringsDifferS(TokenStrings[TOKEN_MINUS], B)) + { + T.Type = TOKEN_MINUS; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } + else if(!StringsDifferS(TokenStrings[TOKEN_ASSIGN], B)) + { + T.Type = TOKEN_ASSIGN; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } + else if(!StringsDifferS(TokenStrings[TOKEN_OPEN_BRACE], B)) + { + T.Type = TOKEN_OPEN_BRACE; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } + else if(!StringsDifferS(TokenStrings[TOKEN_CLOSE_BRACE], B)) + { + T.Type = TOKEN_CLOSE_BRACE; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } + else if(!StringsDifferS(TokenStrings[TOKEN_SEMICOLON], B)) + { + T.Type = TOKEN_SEMICOLON; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } + else if(IsValidIdentifierCharacter(*B->Ptr)) + { + T.Type = TOKEN_IDENTIFIER; + T.Content.Base = B->Ptr; + while(IsValidIdentifierCharacter(*B->Ptr)) + { + ++T.Content.Length; + ++B->Ptr; + } + Advancement = 0; + } + else if(IsNumber(*B->Ptr)) + { + T.Type = TOKEN_NUMBER; + T.Content.Base = B->Ptr; + while(IsNumber(*B->Ptr)) + { + ++T.Content.Length; + ++B->Ptr; + } + Advancement = 0; + } + else if(*B->Ptr == '\n') + { + T.Type = TOKEN_NEWLINE; + T.Content.Base = B->Ptr; + T.Content.Length = 1; + ++Result->CurrentLine; + Advancement = 1; } else { - ++B->Ptr; + T.Type = TOKEN_NULL; + string Char = GetUTF8Character(B->Ptr, B->Size - (B->Ptr - B->Location)); + Advancement = Char.Length; + string Filepath = Wrap0(Result->File.Path); + if(Char.Base) + { + ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Unhandled character (ignored): ", &Char); + } + else + { + ConfigErrorInt(&Filepath, Result->CurrentLine, S_WARNING, "Malformed UTF-8 bytes encountered (skipped): ", Char.Length); + } } - } - T.Content.Length = B->Ptr - T.Content.Base; - Advancement = 0;//StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); - } - else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) - { - Advancement = 2; - T.Type = TOKEN_NULL; - string Char = { .Base = B->Ptr, .Length = 2 }; - string Filepath = Wrap0(Result->File.Path); - ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Mismatched closing multiline comment marker: ", &Char); - } - else if(!StringsDifferS(TokenStrings[TOKEN_DOUBLEQUOTE], B)) - { - T.Type = TOKEN_STRING; - ++B->Ptr; - T.Content.Base = B->Ptr; - while(B->Ptr - B->Location < B->Size && *B->Ptr != '"') - { - if(*B->Ptr == '\\') - { - ++T.Content.Length; - ++B->Ptr; - } - ++T.Content.Length; - ++B->Ptr; - } - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_MINUS], B)) - { - T.Type = TOKEN_MINUS; - T.Content.Base = B->Ptr; - ++T.Content.Length; - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_ASSIGN], B)) - { - T.Type = TOKEN_ASSIGN; - T.Content.Base = B->Ptr; - ++T.Content.Length; - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_OPEN_BRACE], B)) - { - T.Type = TOKEN_OPEN_BRACE; - T.Content.Base = B->Ptr; - ++T.Content.Length; - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_CLOSE_BRACE], B)) - { - T.Type = TOKEN_CLOSE_BRACE; - T.Content.Base = B->Ptr; - ++T.Content.Length; - Advancement = 1; - } - else if(!StringsDifferS(TokenStrings[TOKEN_SEMICOLON], B)) - { - T.Type = TOKEN_SEMICOLON; - T.Content.Base = B->Ptr; - ++T.Content.Length; - Advancement = 1; - } - else if(IsValidIdentifierCharacter(*B->Ptr)) - { - T.Type = TOKEN_IDENTIFIER; - T.Content.Base = B->Ptr; - while(IsValidIdentifierCharacter(*B->Ptr)) - { - ++T.Content.Length; - ++B->Ptr; - } - Advancement = 0; - } - else if(IsNumber(*B->Ptr)) - { - T.Type = TOKEN_NUMBER; - T.Content.Base = B->Ptr; - while(IsNumber(*B->Ptr)) - { - ++T.Content.Length; - ++B->Ptr; - } - Advancement = 0; - } - else if(*B->Ptr == '\n') - { - T.Type = TOKEN_NEWLINE; - T.Content.Base = B->Ptr; - T.Content.Length = 1; - ++Result->CurrentLine; - Advancement = 1; - } - else - { - T.Type = TOKEN_NULL; - string Char = GetUTF8Character(B->Ptr, B->Size - (B->Ptr - B->Location)); - Advancement = Char.Length; - string Filepath = Wrap0(Result->File.Path); - if(Char.Base) - { - ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Unhandled character (ignored): ", &Char); - } - else - { - ConfigErrorInt(&Filepath, Result->CurrentLine, S_WARNING, "Malformed UTF-8 bytes encountered (skipped): ", Char.Length); - } - } - PushToken(Result, &T); - Advance(B, Advancement); - SkipWhitespace(Result, B); - } + PushToken(Result, &T); + Advance(B, Advancement); + SkipWhitespace(Result, B); + } + } break; } - else - { - ConfigError(0, 0, S_WARNING, "Unable to open config file: ", &Path); - } - Free(Path0); return Result; } @@ -2341,7 +2355,8 @@ ScopeTokens(scope_tree *Tree, memory_book *TokensList, tokens *T, memory_book *T } if(!I) { - I = Tokenise(TokensList, IncludePathL); + token *This = GetPlaceInBook(&T->Token, IncludePathTokenIndex); + I = Tokenise(TokensList, IncludePathL, &Filepath, This->LineNumber); } if(I) { @@ -2352,11 +2367,6 @@ ScopeTokens(scope_tree *Tree, memory_book *TokensList, tokens *T, memory_book *T return 0; } } - else - { - token *This = GetPlaceInBook(&T->Token, IncludePathTokenIndex); - ConfigFileIncludeError(&Filepath, This->LineNumber, Wrap0(IncludePath)); - } Free(IncludePath); } else @@ -4977,7 +4987,7 @@ ParseConfig(string Path, memory_book *TokensList) MEM_LOOP_POST("InitTypeSpecs") #endif config *Result = 0; - tokens *T = Tokenise(TokensList, Path); + tokens *T = Tokenise(TokensList, Path, NA, NA); #if 0 MEM_LOOP_PRE_FREE("Tokenise") @@ -4993,7 +5003,7 @@ ParseConfig(string Path, memory_book *TokensList) scope_tree *ScopeTree = InitRootScopeTree(); SetTypeSpec(ScopeTree, &TypeSpecs); SetDefaults(ScopeTree, &TypeSpecs); - ScopeTree = ScopeTokens(ScopeTree, TokensList, T, &TypeSpecs, 0); + ScopeTree = ScopeTokens(ScopeTree, TokensList, T, &TypeSpecs, NA); // TODO(matt): Mem testing //