Skip to content

Commit

Permalink
Further work done on #328 : Portable Application Mode.
Browse files Browse the repository at this point in the history
In that mode, the registry is only used for a few things, such as registering the qiqqa:// URI and qiaa_backup file extension for qiqqa backup archives. Otherwise, all settings (such as Qiqqa Base Directory) are obtained from the "Qiqqa.Portable.Settings.json5" JSON5-formatted configuration file, that must be present in the same directory as the qiqqa.exe executable for it to be recognized as a Portable Application.

The technique used is to make a Portable Application a kind of "developer override" mode: the same files are involved and the way configuration bits and pieces are specified/overridden is the same.

Re "developer override mode":

Now we support the "Qiqqa.Developer.Settings.json5" developer config file in the application directory too! (Next to that "Qiqqa.Portable.Settings.json5" file)

When we have determined the Base Directory in Qiqqa, we also load the *other* "Qiqqa.Developer.Settings.json5" file that may be available there: hence we now support a chain of 2..3 developer override config files, all formatted the same way, and loaded in this order

- "Qiqqa.Portable.Settings.json5"   in Qiqqa.exe's directory
- "Qiqqa.Developer.Settings.json5"  in Qiqqa.exe's directory
- (determine Base Directory by checking the files above and optionally, the Windows Registry)
- "Qiqqa.Developer.Settings.json5"  in the Base Directory

All these files are optional; the EXISTANCE of the "Qiqqa.Portable.Settings.json5" file determines whether Qiqqa acts like a Portable Application or as a regular install - the file may even be empty then!

Extra: the developer config files can now also override the registry entries used by Qiqqa for various configuration settings:

{
  "DebugConsole": false,
  "AllowMultipleQiqqaInstances": true,
  "BaseDataDirectory": "Z:\\lib\\tooling\\qiqqa\\Qiqqa\\bin\\Debug\\My.Qiqqa.Libraries",
  "LoadKnownWebLibraries": true,
  "AddLegacyWebLibrariesThatCanBeFoundOnDisk": true,
  "SaveKnownWebLibraries": true,
  "DoInterestingAnalysis_GoogleScholar": false,
  "FolderWatcher": true,
  "TextExtraction": true,
  "SuggestingMetadata": true,
  "BuildSearchIndex": true,
  "RenderPDFPagesForSidePanels": true,
  "RenderPDFPagesForReading": true,
  "RenderPDFPagesForOCR": true,
  "FirstInstallNotification": "83"
}
  • Loading branch information
GerHobbelt committed Jun 10, 2021
1 parent 1dbc2b5 commit 7e57cb2
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 65 deletions.
63 changes: 14 additions & 49 deletions Qiqqa/Common/Configuration/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,11 @@ public string ProgramHTMLToPDF
get => Path.Combine(StartupDirectoryForQiqqa, @"wkhtmltopdf.exe");
}

public string DeveloperTestSettingsFilename
public string DeveloperTestSettingsFilename_2_LibsBase
{
get => Path.Combine(BaseDirectoryForQiqqa, @"Qiqqa.Developer.Settings.json5");
}

private Dictionary<string, object> developer_test_settings = null;
private ConfigurationRecord configuration_record;
private AugmentedBindable<ConfigurationRecord> configuration_record_bindable;

Expand Down Expand Up @@ -257,37 +256,6 @@ public void ResetConfigurationRecord()
configuration_record.Feedback_GATrackingCode = Guid.NewGuid().ToString();
}
#endif

// Also see if we have a Developer Test Settings file, which contains development/test environment overrides:
try
{
if (File.Exists(DeveloperTestSettingsFilename))
{
Logging.Info("Loading developer test settings file {0}", DeveloperTestSettingsFilename);

// see also https://www.newtonsoft.com/json/help/html/SerializationErrorHandling.htm

List<string> errors = new List<string>();

developer_test_settings = JsonConvert.DeserializeObject<Dictionary<string, object>>(
File.ReadAllText(DeveloperTestSettingsFilename),
new JsonSerializerSettings
{
Error = delegate (object sender, ErrorEventArgs args)
{
errors.Add(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
},
//Converters = { new IsoDateTimeConverter() }
});

Logging.Info("Loaded developer test settings file {0}: {1}", DeveloperTestSettingsFilename, errors.Count == 0 ? "no errors" : errors.ToString());
}
}
catch (Exception ex)
{
Logging.Error(ex, "There was a problem loading developer test settings file {0}", DeveloperTestSettingsFilename);
}
}

private void configuration_record_bindable_PropertyChanged(object sender, PropertyChangedEventArgs e)
Expand Down Expand Up @@ -441,32 +409,27 @@ public ConfigurationRecord ConfigurationRecord
/// <returns>`true` by default (ENabled), unless the loaded developer overrides file explicitly set this key to `false`.</returns>
public static bool IsEnabled(string key)
{
if (null != Instance.developer_test_settings)
ASSERT.Test(null != UserRegistry.DeveloperOverridesDB);

if (UserRegistry.DeveloperOverridesDB.TryGetValue(key, out var val))
{
if (Instance.developer_test_settings.TryGetValue(key, out var val))
{
bool? rv = val as bool?;
return rv ?? (key == "DoInterestingAnalysis_GoogleScholar" ? false : true);
}
bool? rv = val as bool?;
return rv ?? (key == "DoInterestingAnalysis_GoogleScholar" ? false : true);
}

return (key == "DoInterestingAnalysis_GoogleScholar" ? false : true);
}

public static void ResetDeveloperSettings()
{
if (null != Instance.developer_test_settings)
{
Instance.developer_test_settings.Clear();
}
ASSERT.Test(null != UserRegistry.DeveloperOverridesDB);

UserRegistry.DeveloperOverridesDB.Clear();
}

public static Dictionary<string, object> GetDeveloperSettingsReference()
{
if (null == Instance.developer_test_settings)
{
Instance.developer_test_settings = new Dictionary<string, object>();
}
return Instance.developer_test_settings;
return UserRegistry.DeveloperOverridesDB;
}

public static void ThrowWhenActionIsNotEnabled(string key)
Expand Down Expand Up @@ -529,9 +492,11 @@ public static Dictionary<string, string> GetCurrentConfigInfos()
rv.Add("SearchHistoryFilename", cfg.SearchHistoryFilename);
rv.Add("Program7ZIP", cfg.Program7ZIP);
rv.Add("ProgramHTMLToPDF", cfg.ProgramHTMLToPDF);
rv.Add("DeveloperTestSettingsFilename", cfg.DeveloperTestSettingsFilename);
rv.Add("DeveloperTestSettingsFilename (Application Level)", UnitTestDetector.DeveloperTestSettingsFilename_1_App);
rv.Add("DeveloperTestSettingsFilename (BaseDirectory Level)", cfg.DeveloperTestSettingsFilename_2_LibsBase);
rv.Add("NoviceVisibility", $"{cfg.NoviceVisibility}");
rv.Add("SearchHistory", string.Join("\n", cfg.SearchHistory));
rv.Add("IsPortableApplication", RegistrySettings.GetPortableApplicationMode().ToString());

StringBuilder s = new StringBuilder();
foreach (var rec in cfg.ConfigurationRecord.GetCurrentConfigInfos())
Expand Down
8 changes: 8 additions & 0 deletions Qiqqa/Common/RegistrySettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Qiqqa.Common.Configuration;
using Utilities;
using Utilities.Misc;

namespace Qiqqa.Common
Expand Down Expand Up @@ -93,5 +95,11 @@ public class RegistrySettings : QuantisleUserRegistry
private RegistrySettings() : base("Qiqqa")
{
}

public static void AugmentDeveloperOverridesDB()
{
// now also check for a developer override config file in the Basedirectory and add those overrides to the set:
UnitTestDetector.AugmentDeveloperConfiguration(ref registry_overrides_db, ConfigurationManager.Instance.DeveloperTestSettingsFilename_2_LibsBase);
}
}
}
3 changes: 2 additions & 1 deletion Qiqqa/Main/FileAssociationRegistration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Qiqqa.Common;
using Qiqqa.Common.Configuration;
using Utilities;
using Utilities.GUI;
Expand Down Expand Up @@ -26,7 +27,7 @@ internal static void DoRegistration()
}
catch (Exception ex)
{
if (Qiqqa.Common.RegistrySettings.Instance.GetPortableApplicationMode())
if (RegistrySettings.GetPortableApplicationMode())
{
string msg = $"The Qiqqa Portable Application failed to register the 'qiqqa://' URI type and Qiqqa-associated file extensions. This means the Portable Application will not respond to qiqqa://... links in web pages and elsewhere, and Qiqqa Portable Application will not automatically start again when you doubleclick on a qiqqa backup archive file to have it restore your libraries' backup.\n\nThe cause of this issue is probably tightly restricted user access rights on your machine. More info may be found and reported at the Qiqqa support website & issue tracker: { WebsiteAccess.Url_Support4Qiqqa }";
Logging.Error(ex, msg);
Expand Down
2 changes: 1 addition & 1 deletion Qiqqa/Main/FirstInstallWebLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static void Check()
string version = "" + ClientVersion.CurrentVersion;
RegistrySettings.Instance.Write(RegistrySettings.FirstInstallNotification, version);

if (!RegistrySettings.Instance.GetPortableApplicationMode())
if (!RegistrySettings.GetPortableApplicationMode())
{
string url = WebsiteAccess.GetOurUrl(WebsiteAccess.OurSiteLinkKind.Welcome) + "?version=" + version;
Process.Start(url);
Expand Down
17 changes: 14 additions & 3 deletions Qiqqa/Main/MainEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,25 @@ private static void DoPreamble()
}
}
#endif

UserRegistry.DetectPortableApplicationMode();

{
string portable_app_config_path = Path.Combine(UnitTestDetector.StartupDirectoryForQiqqa, @"Qiqqa.Portable.Settings.json5");
if (File.Exists(portable_app_config_path))
if (UserRegistry.GetPortableApplicationMode())
{
RegistrySettings.Instance.SetPortableApplicationMode(portable_app_config_path);
// set up defaults when they are absent:
object v = null;
UserRegistry.DeveloperOverridesDB.TryGetValue("BaseDataDirectory", out v);
if (string.IsNullOrEmpty(v as string))
{
UserRegistry.DeveloperOverridesDB.Add("BaseDataDirectory", Path.GetFullPath(Path.Combine(UnitTestDetector.StartupDirectoryForQiqqa, @"../My.Qiqqa.Libraries")));
}
}
}

// now also check for a developer override config file in the Basedirectory and add those overrides to the set:
RegistrySettings.AugmentDeveloperOverridesDB();

if (RegistrySettings.Instance.IsSet(RegistrySettings.DebugConsole))
{
Console.Instance.Init();
Expand Down
137 changes: 137 additions & 0 deletions Utilities/DetectUnitTestRunner.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Utilities.Misc;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using File = Alphaleonis.Win32.Filesystem.File;
Expand Down Expand Up @@ -113,6 +115,141 @@ public static bool IsRunningInUnitTest
}
}
});

public static string StartupDirectoryForQiqqa => _StartupDirectoryForQiqqa.Value;

public static string PortableApplicationConfigFilename => Path.Combine(StartupDirectoryForQiqqa, @"Qiqqa.Portable.Settings.json5");

public static string DeveloperTestSettingsFilename_1_App => Path.Combine(StartupDirectoryForQiqqa, @"Qiqqa.Developer.Settings.json5");


// TODO: refactor the next bit; plonking it in here for now until I know how I want to untangle the otherwise circular dependencies of Utilities <-> Qiqqa namespaces.


public static bool HasPortableApplicationConfigFilename()
{
return File.Exists(PortableApplicationConfigFilename);
}

public static Dictionary<string, object> LoadDeveloperConfiguration()
{
// Procedure:
// - load PortableApplication config record, if available
// - load application-level developer config record, if available: override existing entries
// - load BaseDirectory-level developer config record, if available: override existing entries

Dictionary<string, object> cfg = LoadConfigFile(PortableApplicationConfigFilename);
foreach (KeyValuePair<string, object> entry in LoadConfigFile(DeveloperTestSettingsFilename_1_App))
{
if (cfg.ContainsKey(entry.Key))
{
cfg.Remove(entry.Key);
}
cfg.Add(entry.Key, entry.Value);
}
return cfg;
}

public static void AugmentDeveloperConfiguration(ref Dictionary<string, object> cfg, string extra_config_filepath)
{
foreach (KeyValuePair<string, object> entry in LoadConfigFile(extra_config_filepath))
{
if (cfg.ContainsKey(entry.Key))
{
cfg.Remove(entry.Key);
}
cfg.Add(entry.Key, entry.Value);
}
}

private static Dictionary<string, object> LoadConfigFile(string cfg_filepath)
{
try
{
if (!String.IsNullOrEmpty(cfg_filepath) && File.Exists(cfg_filepath))
{
Logging.Info("Loading developer test settings file {0}", cfg_filepath);

// see also https://www.newtonsoft.com/json/help/html/SerializationErrorHandling.htm

List<string> errors = new List<string>();

Dictionary<string, object> developer_test_settings = JsonConvert.DeserializeObject<Dictionary<string, object>>(
File.ReadAllText(cfg_filepath),
new JsonSerializerSettings
{
Error = delegate (object sender, ErrorEventArgs args)
{
errors.Add(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
},
//Converters = { new IsoDateTimeConverter() }
});

Logging.Info("Loaded developer test settings file {0}: {1}", cfg_filepath, errors.Count == 0 ? "no errors" : errors.ToString());

return developer_test_settings;
}
}
catch (Exception ex)
{
Logging.Error(ex, "There was a problem loading developer test settings file {0}", cfg_filepath);
}

return new Dictionary<string, object>();
}

public static void SavePortableApplicationConfiguration(Dictionary<string, object> cfg)
{
string cfg_filepath = PortableApplicationConfigFilename;
// UnitTestDetector.SavePortableApplicationConfiguration(registry_overrides_db);

try
{
// only save config to REPLACE an already existing config file
if (!String.IsNullOrEmpty(cfg_filepath) && File.Exists(cfg_filepath))
{
Logging.Info("Saving portable application settings file {0}", cfg_filepath);

#if false
// Note: only replace/update existing config entries; anything else should have landed
// in the Developer Test config file(s).
//
// The exceptions to this rule are ... TODO
Dictionary<string, object> old_cfg = LoadConfigFile(cfg_filepath);

//... TODO
#endif

string json = JsonConvert.SerializeObject(cfg, Formatting.Indented);

// keep all comments in the JSON5 file which precede the config data:
string[] old_json = File.ReadAllLines(cfg_filepath);
int i;
for (i = 0; i < old_json.Length; i++)
{
if (old_json[i].StartsWith("{"))
break;
}
string header = "";
if (i > 0)
{
header = String.Join("\n", old_json, 0, i) + "\n";
}
json = header + json;

string new_filepath = cfg_filepath + ".new";
File.WriteAllText(new_filepath, json);
File.Delete(cfg_filepath);
File.Move(new_filepath, cfg_filepath);

Logging.Info("Saved portable application settings file {0}", cfg_filepath);
}
}
catch (Exception ex)
{
Logging.Error(ex, "There was a problem saving portable application settings file {0}", cfg_filepath);
}
}
}
}
Loading

0 comments on commit 7e57cb2

Please sign in to comment.