ecere/sys/GlobalSettings: add GlobalSettings::settingsDirectory property to support...
[sdk] / ecere / src / sys / GlobalAppSettings.ec
1 namespace sys;
2
3 #if defined(__WIN32__)
4 #define WIN32_LEAN_AND_MEAN
5 #define UNICODE
6 #define String String_
7 #define strlen _strlen
8 #include <windows.h>
9 #undef String
10 #undef strlen
11 #endif
12
13 import "System"
14 import "Container"
15
16 public enum GlobalSettingType
17 {
18    integer,
19    singleString,
20    stringList
21 };
22
23 public enum SettingsIOResult { error, success, fileNotFound, fileNotCompatibleWithDriver };
24
25 public class GlobalSettingsDriver
26 {
27    class_data const char * name;
28
29    class_property const char * name
30    {
31       set { class_data(name) = value; }
32       get { return class_data(name); }
33    }
34
35 public:
36    virtual SettingsIOResult ::Load(File f, GlobalSettings globalSettings);
37    virtual SettingsIOResult ::Save(File f, GlobalSettings globalSettings);
38
39 }
40
41 static subclass(GlobalSettingsDriver) GetGlobalSettingsDriver(const char * driverName)
42 {
43    OldLink link;
44    for(link = class(GlobalSettingsDriver).derivatives.first; link; link = link.next)
45    {
46       subclass(GlobalSettingsDriver) driver = link.data;
47       if(driver.name && !strcmp(driver.name, driverName))
48          return driver;
49    }
50
51    /*{
52       Module module;
53       char moduleName[MAX_LOCATION];
54       sprintf(moduleName, "ecere%s", driverName);
55       if(module = eModule_Load(__thisModule.application, moduleName, publicAccess))
56       {
57          //Class dsdClass = class(GlobalSettingsDriver);
58          Class dsdClass = eSystem_FindClass(module /-__thisModule.application-/, "GlobalSettingsDriver");
59
60          for(link = dsdClass.derivatives.first; link; link = link.next)
61          {
62             subclass(GlobalSettingsDriver) driver = link.data;
63             if(driver.name && !strcmp(driver.name, driverName))
64                return driver;
65          }
66       }
67    }*/
68    return null;
69 }
70
71 public class GlobalSettingsData
72 {
73 }
74
75 public class GlobalSettings
76 {
77 public:
78    property const char * settingsName
79    {
80       set { delete settingsName; if(value && value[0]) settingsName = CopyString(value); }
81       get { return settingsName; }
82    };
83    property const char * settingsExtension
84    {
85       set { delete settingsExtension; if(value && value[0]) settingsExtension = CopyString(value); }
86       get { return settingsExtension; }
87    };
88    property const char * settingsDirectory
89    {
90       set { delete settingsDirectory; if(value && value[0]) settingsDirectory = CopyUnixPath(value); }
91       get { return settingsDirectory; }
92    };
93    property const char * settingsLocation
94    {
95       set { delete settingsLocation; if(value && value[0]) settingsLocation = CopyUnixPath(value); }
96       get { return settingsLocation; }
97    };
98    property const char * settingsFilePath
99    {
100       set { delete settingsFilePath; if(value && value[0]) settingsFilePath = CopyUnixPath(value); }
101       get { return settingsFilePath; }
102    };
103    property bool allowDefaultLocations
104    {
105       set { allowDefaultLocations = value; }
106       get { return allowDefaultLocations; }
107    };
108    property bool allUsers
109    {
110       set { allUsers = value; }
111       get { return allUsers; }
112    };
113
114    property const String driver
115    {
116       set
117       {
118          driverClass = null;
119          if(value)
120             driverClass = GetGlobalSettingsDriver(value);
121       }
122       get { return driverClass ? driverClass.name : null; }
123    }
124    GlobalSettingsData data;
125    GlobalSettingsData * dataOwner;
126    subclass(GlobalSettingsData) dataClass;
127    bool portable;
128
129 private:
130    char * settingsName;
131    char * settingsExtension;
132    char * settingsLocation;
133    char * settingsFilePath;
134    bool allowDefaultLocations;
135    bool allUsers;
136    bool globalPath;
137    char * settingsDirectory;
138
139    FileMonitor settingsMonitor
140    {
141       this, fileChange = { modified = true };
142
143       bool OnFileNotify(FileChange action, const char * param)
144       {
145          OnAskReloadSettings();
146          return true;
147       }
148    };
149    File f;
150    bool locked;
151
152    subclass(GlobalSettingsDriver) driverClass;
153
154    ~GlobalSettings()
155    {
156       if(f)
157          f.Unlock(0, 0, true);
158       delete f;
159       delete settingsName;
160       delete settingsExtension;
161       delete settingsLocation;
162       delete settingsFilePath;
163       delete settingsDirectory;
164    }
165
166    char * PrepareSpecifiedLocationPath(const char * extension, bool create)
167    {
168       char * path = null;
169       char location[MAX_LOCATION] = "";
170       if(settingsLocation)
171          strcpy(location, settingsLocation);
172       path = GetFilePath(location, extension, create, false, false);
173       return path;
174    }
175
176    char * PreparePortablePath(const char * extension, bool create)
177    {
178       //
179       char * path = null;
180       char location[MAX_LOCATION];
181       LocateModule(null, location);
182       StripLastDirectory(location, location);
183       path = GetFilePath(location, extension, create, false, false);
184       return path;
185    }
186
187    char * PrepareHomePath(const char * extension, bool create, bool unixStyle)
188    {
189       // ~/.apprc
190       char * path = null;
191       char * home = getenv("HOME");
192       if(home && home[0])
193       {
194          char location[MAX_LOCATION];
195          strcpy(location, home);
196          path = GetFilePath(location, extension, create, true, unixStyle);
197       }
198       return path;
199    }
200
201 #if defined(__WIN32__)
202    char * PrepareUserProfilePath(const char * extension, bool create)
203    {
204       // Windows attempts: $USERPROFILE/app.ini
205       char * path = null;
206       char * profile = getenv("USERPROFILE");
207       if(profile && profile[0])
208       {
209          char location[MAX_LOCATION];
210          strcpy(location, profile);
211          path = GetFilePath(location, extension, create, false, false);
212       }
213       return path;
214    }
215
216    char * PrepareHomeDrivePath(const char * extension, bool create)
217    {
218       char * path = null;
219       const char * homedrive = getenv("HOMEDRIVE");
220       const char * homepath = getenv("HOMEPATH");
221       if(homedrive && homedrive[0] && homepath && homepath[0])
222       {
223          char location[MAX_LOCATION];
224          strcpy(location, homedrive);
225          PathCatSlash(location, homepath);
226          path = GetFilePath(location, extension, create, false, false);
227       }
228       return path;
229    }
230
231    char * PrepareSystemPath(const char * extension, bool create)
232    {
233       char * path = null;
234       uint16 _wfilePath[MAX_LOCATION];
235       char location[MAX_LOCATION];
236       GetSystemDirectory(_wfilePath, MAX_LOCATION);
237       UTF16toUTF8Buffer(_wfilePath, location, MAX_LOCATION);
238       path = GetFilePath(location, extension, create, false, false);
239       return path;
240    }
241
242    char * PrepareAllUsersPath(const char * extension, bool create)
243    {
244       char * path = null;
245       char * allUsers = getenv("ALLUSERSPROFILE");
246       if(allUsers && allUsers[0])
247       {
248          char location[MAX_LOCATION];
249          strcpy(location, allUsers);
250          path = GetFilePath(location, extension, create, false, false);
251       }
252       return path;
253    }
254 #else
255    char * PrepareEtcPath(const char * extension, bool create)
256    {
257       char * path = null;
258       char location[MAX_LOCATION] = "/etc/";
259       path = GetFilePath(location, extension, create, false, false);
260       return path;
261    }
262 #endif
263
264    char * GetFilePath(char * location, const char * extension, bool create, bool dotPrefix, bool runCommandsStyle)
265    {
266       char * path = null;
267       FileAttribs attribs;
268       if(location[0])
269          MakeSlashPath(location);
270       if(location[0] && (attribs = FileExists(location)).isDirectory)
271       {
272          if(settingsDirectory)
273          {
274             if(dotPrefix)
275             {
276                PathCatSlash(location, ".");
277                strcat(location, settingsDirectory);
278             }
279             else
280                PathCatSlash(location, settingsDirectory);
281             if(create)
282                MakeDir(location);
283             attribs = FileExists(location);
284          }
285          if(attribs.isDirectory)
286          {
287             char * name = new char[strlen(settingsName) + strlen(extension) + 2];
288             if(dotPrefix && !settingsDirectory)
289             {
290                strcpy(name, ".");
291                strcat(name, settingsName);
292             }
293             else
294                strcpy(name, settingsName);
295             if(runCommandsStyle)
296                strcat(name, "rc");
297             else
298             {
299                strcat(name, ".");
300                strcat(name, extension);
301             }
302             path = new char[strlen(location) + strlen(name) + 16];
303             strcpy(path, location);
304             PathCatSlash(path, name);
305             delete name;
306          }
307       }
308       return path;
309    }
310
311    const char * GetExtension()
312    {
313       const char * extension;
314       if(settingsExtension)
315          extension = settingsExtension;
316       else
317 #if defined(__WIN32__)
318          extension = "ini";
319 #else
320          extension = "conf";
321 #endif
322       return extension;
323    }
324
325    void FileOpenTryRead()
326    {
327       f = FileOpen(settingsFilePath, read);
328       //PrintLn("GlobalSettings::FileOpenTryRead -- ", settingsFilePath, " -- ", f != null);
329       if(!f)                       // This delete will cover both trying the next possible config location and
330          delete settingsFilePath;  // the case when we're doing a load when the config file is no longer available
331    }                               // and we want to re-try all possible config locations.
332
333    bool FileOpenTryWrite(bool shouldDelete, bool * locked)
334    {
335       *locked = false;
336       f = FileOpen(settingsFilePath, readWrite);
337       if(!f)
338       {
339          f = FileOpen(settingsFilePath, writeRead);
340          if(!driverClass)
341          {
342             delete f;
343             f = FileOpen(settingsFilePath, readWrite);
344          }
345       }
346       if(f)
347       {
348          // Don't wait for a lock, first one to lock gets to write, other will likely loose changes on a reload.
349          if(f.Lock(exclusive, 0, 0, false))
350          {
351             *locked = true;
352             if(driverClass)
353             {
354                f.Truncate(0);
355                f.Seek(0, start);
356             }
357          }
358       }
359       else if(shouldDelete)        // This delete will cover both trying the next possible config location and
360          delete settingsFilePath;  // allow trying to save to a location where user has permission.
361       //PrintLn("GlobalSettings::FileOpenTryWrite -- ", settingsFilePath, " -- ", f != null);
362       return f != null;
363    }
364
365 public:
366    virtual void OnAskReloadSettings();
367
368    bool OpenAndLock(FileSize * fileSize)
369    {
370       if(!f)
371       {
372          settingsMonitor.StopMonitoring();
373
374          if(settingsFilePath)
375             FileOpenTryRead();
376
377          if(!settingsFilePath && settingsName && settingsName[0])
378          {
379             const char * extension = GetExtension();
380
381             if(!f && (settingsFilePath = PrepareSpecifiedLocationPath(extension, false)))
382                FileOpenTryRead();
383             if(!f && (!settingsLocation || allowDefaultLocations))
384             {
385                globalPath = false;
386                if(!f && (settingsFilePath = PreparePortablePath(extension, false)))
387                   FileOpenTryRead();
388                if(f)
389                   portable = true;
390                if(!allUsers)
391                {
392 #if defined(__WIN32__)
393                   if(!f && (settingsFilePath = PrepareHomePath(extension, false, false)))
394                      FileOpenTryRead();
395 #endif
396                   if(!f && (settingsFilePath = PrepareHomePath(extension, false, true)))
397                      FileOpenTryRead();
398                }
399 #if defined(__WIN32__)
400                if(!allUsers)
401                {
402                   if(!f && (settingsFilePath = PrepareUserProfilePath(extension, false)))
403                      FileOpenTryRead();
404                   if(!f && (settingsFilePath = PrepareHomeDrivePath(extension, false)))
405                      FileOpenTryRead();
406                }
407                if(!f)
408                   globalPath = true;
409                if(!f && (settingsFilePath = PrepareAllUsersPath(extension, false)))
410                   FileOpenTryRead();
411
412                if(!f && (settingsFilePath = PrepareSystemPath(extension, false)))
413                   FileOpenTryRead();
414 #else
415                if(!f)
416                   globalPath = true;
417                if(!f && (settingsFilePath = PrepareEtcPath(extension, false)))
418                   FileOpenTryRead();
419 #endif
420             }
421          }
422       }
423       if(f)
424       {
425          int c;
426          if(!locked)
427          {
428             // At some point wait was true, it was changed to false and now we do retries.
429             // Could it be because the wait to true was causing blocking behaviors?
430             //if(f && f.Lock(shared, 0, 0, true)) <- I think the wait to true is bad, it wrote blanked out global settings.
431             for(c = 0; c < 10 && !(locked = f.Lock(shared, 0, 0, false)); c++)
432             {
433                ecere::sys::Sleep(0.01);
434             }
435          }
436
437          if(locked && fileSize)
438             *fileSize = f.GetSize();
439       }
440       return f && locked;
441    }
442
443    virtual SettingsIOResult Load()
444    {
445       SettingsIOResult result = fileNotFound;
446       if(!f || !locked)
447          OpenAndLock(null);
448
449       if(f && locked)
450       {
451          if(driverClass)
452             result = driverClass.Load(f, this);
453          else
454             result = success;
455       }
456       return result;
457    }
458
459    virtual SettingsIOResult Save()
460    {
461       SettingsIOResult result = error;
462       if(!f)
463       {
464          locked = false;
465
466          settingsMonitor.StopMonitoring();
467
468          if(settingsFilePath)
469             // Don't auto delete settingsFilePath because only want to try another path if we were using a global path
470             FileOpenTryWrite(false, &locked);
471
472          if((!settingsFilePath || (!f && globalPath)) && settingsName && settingsName[0])
473          {
474             const char * extension = GetExtension();
475             delete settingsFilePath;
476
477             if(!f && (settingsFilePath = PrepareSpecifiedLocationPath(extension, true)))
478                FileOpenTryWrite(true, &locked);
479             if(!f && (!settingsLocation || allowDefaultLocations))
480             {
481                globalPath = true;
482                // never try to write a new portable configuration file?
483                //       -- Probably always want to write back to the same file, the first FileOpenTryWrite(true) should succeed
484                //if(!f && (settingsFilePath = PreparePortablePath(extension, true)))
485                //   FileOpenTryWrite(true);
486 #if defined(__WIN32__)
487                if(!f && (settingsFilePath = PrepareAllUsersPath(extension, true)))
488                   FileOpenTryWrite(true, &locked);
489 #else
490                if(!f && (settingsFilePath = PrepareEtcPath(extension, true)))
491                   FileOpenTryWrite(true, &locked);
492 #endif
493                if(!f && allUsers)
494                {
495                   globalPath = false;
496                   if(!f && (settingsFilePath = PrepareHomePath(extension, true,
497 #if defined(__WIN32__)
498                      false
499 #else
500                      true
501 #endif
502                      )))
503                      FileOpenTryWrite(true, &locked);
504                }
505 #if defined(__WIN32__)
506                if(!f && !allUsers)
507                {
508                   globalPath = false;
509                   if(!f && (settingsFilePath = PrepareUserProfilePath(extension, true)))
510                      FileOpenTryWrite(true, &locked);
511                   if(!f && (settingsFilePath = PrepareHomeDrivePath(extension, true)))
512                      FileOpenTryWrite(true, &locked);
513                }
514                if(!f && (settingsFilePath = PrepareSystemPath(extension, true)))
515                {
516                   globalPath = true;
517                   FileOpenTryWrite(true, &locked);
518                }
519 #endif
520             }
521          }
522          if(f && locked)
523          {
524             if(driverClass)
525                result = driverClass.Save(f, this);
526             else
527                result = success;
528          }
529       }
530       return result;
531    }
532
533    void Close()
534    {
535       if(f)
536       {
537          settingsMonitor.StopMonitoring();
538          f.Unlock(0,0,true);
539          locked = false;
540          delete f;
541       }
542    }
543
544    void CloseAndMonitor()
545    {
546       Close();
547       if(settingsFilePath && OnAskReloadSettings != GlobalSettings::OnAskReloadSettings)
548       {
549          settingsMonitor.fileName = settingsFilePath;
550          settingsMonitor.StartMonitoring();
551       }
552    }
553 }
554
555 public class GlobalAppSettings : GlobalSettings
556 {
557 public:
558    bool GetGlobalValue(const char * section, const char * name, GlobalSettingType type, void * value)
559    {
560       bool result = false;
561       if(f)
562       {
563          char line[92048];
564          int lenSection = strlen(section);
565          int lenName = strlen(name);
566          f.Seek(0, start);
567          while(f.GetLine(line, sizeof(line)))
568          {
569             if(line[0] == '[' && !strncmp(line+1, section, lenSection) && line[lenSection+1] == ']')
570                break;
571          }
572          if(!f.Eof())
573          {
574             while(f.GetLine(line, sizeof(line)))
575             {
576                if(!strncmp(line, name, lenName) && line[lenName] == '=')
577                {
578                   char * string = line + lenName + 1;
579                   switch(type)
580                   {
581                      case stringList:
582                      {
583                         int c;
584                         Container<String> list = value;
585                         char * tokens[256];
586                         int numTokens = TokenizeWith(string,
587                            sizeof(tokens) / sizeof(byte *), tokens, " ,", false);
588                         list.Free();
589                         for(c = 0; c<numTokens; c++)
590                         {
591                            list.Add(CopyString(tokens[c]));
592                         }
593                         break;
594                      }
595                      case singleString:
596                      {
597                         char ** thisString = value;
598                         *thisString = CopyString(string);
599                         break;
600                      }
601                      case integer:
602                      {
603                         int * integer = value;
604                         *integer = (int)strtol(string, null, 0);
605                         break;
606                      }
607                   }
608                   result = true;
609                   break;
610                }
611             }
612          }
613       }
614       return result;
615    }
616
617    bool PutGlobalValue(const char * section, const char * name, GlobalSettingType type, const void * value)
618    {
619       bool result = false;
620       if(f)
621       {
622          char line[92048], outputLine[92048] = "";
623          int lenSection = strlen(section);
624          int lenName = strlen(name);
625          int lenOutput;
626          byte * remainingBuffer = null;
627          int remainingLen = 0;
628          uint currentSize = 0, newSize = 0;
629
630          f.Seek(0, start);
631
632          // Prepare the output line
633          strcpy(outputLine, name);
634          strcat(outputLine, "=");
635          switch(type)
636          {
637             case stringList:
638             {
639                Container<String> list = (void *)value;
640                Iterator<String> item { list };
641                item.Next();
642                while(item.pointer)
643                {
644                   strcat(outputLine, "\"");
645                   if(item.data)
646                      strcat(outputLine, item.data);
647                   strcat(outputLine, "\"");
648                   item.Next();
649                   if(item.pointer)
650                      strcat(outputLine, ",");
651                }
652                break;
653             }
654             case singleString:
655                // strcat(outputLine, "\"");
656                if(value)
657                   strcat(outputLine, value);
658                // strcat(outputLine, "\"");
659                break;
660             case integer:
661             {
662                char integer[64];
663                sprintf(integer, "%d", (int)(intptr)value);
664                strcat(outputLine, integer);
665                break;
666             }
667          }
668 #if defined(__WIN32__)
669          strcat(outputLine, "\r\n");
670 #else
671          strcat(outputLine, "\n");
672 #endif
673          lenOutput = strlen(outputLine);
674
675          while(f.GetLine(line, sizeof(line)))
676          {
677             if(line[0] == '[' && !strncmp(line+1, section, lenSection) && line[lenSection+1] == ']')
678                break;
679          }
680          if(!f.Eof())
681          {
682             int posBeforeSection = -1;
683             for(;;)
684             {
685                uint pos = f.Tell();
686                if(!f.GetLine(line, sizeof(line)))
687                   break;
688                if(posBeforeSection == -1 || !line[0])
689                   posBeforeSection = pos;
690
691                if(line[0] == '[')
692                {
693                   // We've already reached next section
694                   f.Seek(posBeforeSection, start);
695
696                   // Remember the rest of the file
697                   while(!f.Eof())
698                   {
699                      remainingBuffer = renew remainingBuffer byte[remainingLen + 65536];
700                      remainingLen += f.Read(remainingBuffer + remainingLen, 1, 65536);
701                   }
702                   // Don't need to truncate. leave currentSize and newSize at 0
703                   f.Seek(posBeforeSection, start);
704                   break;
705                }
706                else if(!strncmp(line, name, lenName) && line[lenName] == '=')
707                {
708                   uint endLinePos = f.Tell();
709
710                   // Remember the rest of the file
711                   while(!f.Eof())
712                   {
713                      remainingBuffer = renew remainingBuffer byte[remainingLen + 65536];
714                      remainingLen += f.Read(remainingBuffer + remainingLen, 1, 65536);
715                   }
716                   currentSize = endLinePos + remainingLen;
717                   newSize = pos + lenOutput + remainingLen;
718
719                   // Go back where we will overwrite this line
720                   f.Seek(pos, start);
721                   break;
722                }
723             }
724          }
725          else
726          {
727             // Add the section
728 #if defined(__WIN32__)
729             f.Printf("\r\n[%s]\r\n", section);
730 #else
731             f.Printf("\n[%s]\n", section);
732 #endif
733          }
734
735          // Write the line
736          f.Write(outputLine, 1, lenOutput);
737
738          // Write back the rest of the file if necessary
739          if(remainingBuffer)
740          {
741             f.Write(remainingBuffer, 1, remainingLen);
742             delete remainingBuffer;
743             if(currentSize != newSize)
744                FileTruncate(settingsFilePath, newSize);
745          }
746          result = true;
747       }
748       return result;
749    }
750 };