18using System.Collections.Generic;
19using System.Diagnostics;
22using System.Threading;
26using Newtonsoft.Json.Linq;
28using static Org.BouncyCastle.Asn1.Cmp.Challenge;
32 internal class ChromeProfileManager
44 private static readonly Random random =
new Random();
48 private const string TempProfilePrefix =
"BrowserProfile_";
52 private const string SweepingSuffix =
".sweeping";
56 private const int AbandonedAfterMinutes = 1;
60 private const int SweepEverySeconds = 30;
62 private static DateTime lastSweep = DateTime.MinValue;
63 private static int sweeping = 0;
70 internal static string NewTempProfilePath()
72 return Path.Combine(GPAL.TempProfileDirectory, $
"{TempProfilePrefix}{Guid.NewGuid():N}");
83 internal static bool DeleteWhenNothingIsUsingIt(
string directory)
89 string renamedTo = directory;
93 if (
false == directory.EndsWith(SweepingSuffix))
95 renamedTo = Path.Combine(Path.GetDirectoryName(directory), $
"{TempProfilePrefix}{Guid.NewGuid():N}{SweepingSuffix}");
96 Directory.Move(directory, renamedTo);
99 Directory.Delete(renamedTo,
true);
117 internal static void SweepAbandonedTempProfiles()
120 if (DateTime.Now < lastSweep.AddSeconds(SweepEverySeconds))
123 if (0 != Interlocked.Exchange(ref sweeping, 1))
126 lastSweep = DateTime.Now;
128 System.Threading.Tasks.Task.Run(() =>
132 string root = GPAL.TempProfileDirectory;
134 if (
false ==
string.IsNullOrEmpty(root) &&
true == Directory.Exists(root))
136 DateTime olderThan = DateTime.Now.AddMinutes(-AbandonedAfterMinutes);
139 foreach (
string directory
in Directory.GetDirectories(root, TempProfilePrefix +
"*"))
143 if (
false == directory.EndsWith(SweepingSuffix) && Directory.GetCreationTime(directory) > olderThan)
146 if (
true == DeleteWhenNothingIsUsingIt(directory))
151 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $
"Cleared [{swept}] temporary browser profile(s) left behind by earlier runs from [{root}]");
157 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, $
"Could not clear old temporary browser profiles",
null, GPALObjectType.None, ex);
161 Interlocked.Exchange(ref sweeping, 0);
166 internal static string CreateTempUserProfile(
167 string downloadDirectory,
168 bool promptForDownload,
169 bool openPDFExternally,
172 Dictionary<string, object> additionalPrefs =
null)
176 if (!Directory.Exists(downloadDirectory))
177 Directory.CreateDirectory(downloadDirectory);
180 string tempProfileDir = NewTempProfilePath();
182 string defaultDir = Path.Combine(tempProfileDir,
"Default");
184 Directory.CreateDirectory(defaultDir);
186 Base64Helper base64Helper =
new Base64Helper();
187 base64Helper.WithInput(GPAL.GPALSettings.Preferences).SaveTo(Path.Combine(defaultDir,
"Preferences"));
188 base64Helper.WithInput(GPAL.GPALSettings.SecurePreferences).SaveTo(Path.Combine(defaultDir,
"Secure Preferences"));
189 base64Helper.WithInput(GPAL.GPALSettings.LocalState).SaveTo(Path.Combine(tempProfileDir,
"Local State"));
195 ApplyDownloadPreferences(tempProfileDir, downloadDirectory, promptForDownload, openPDFExternally);
197 ApplyImagePreference(tempProfileDir, loadImages);
199 if (
null != additionalPrefs && 0 < additionalPrefs.Count)
200 ApplyAdditionalPreferences(tempProfileDir, additionalPrefs);
202 return tempProfileDir;
206 GPAL.PublishSimpleEvent(
207 Enums.GPALEventType.EXCEPTION,
208 $
"Failed creating temp Chrome profile folder",
210 Enums.GPALObjectType.None,
229 private static readonly
string[] downloadPreferenceKeys =
231 "download.prompt_for_download",
232 "download.default_directory",
233 "plugins.always_open_pdf_externally",
234 "browser.show_hub_popup_on_download_start",
245 private static void SetByPath(JObject root,
string path, JToken value)
247 string[] parts = path.Split(
'.');
250 for (
int idx = 0; idx < parts.Length - 1; idx++)
252 if (
false == (node[parts[idx]] is JObject child))
254 child =
new JObject();
255 node[parts[idx]] = child;
261 node[parts[parts.Length - 1]] = value;
269 private static string PreferencesFileFor(
string profileDirectory)
271 string preferencesPath =
null;
273 if (
false ==
string.IsNullOrEmpty(profileDirectory))
275 string inDefault = Path.Combine(profileDirectory,
"Default",
"Preferences");
276 string inProfile = Path.Combine(profileDirectory,
"Preferences");
278 preferencesPath = File.Exists(inDefault) ? inDefault : File.Exists(inProfile) ? inProfile :
null;
281 return preferencesPath;
295 internal static Dictionary<string, JToken> ApplyDownloadPreferences(
string profileDirectory,
string downloadDirectory,
bool promptForDownload,
bool openPDFExternally)
297 Dictionary<string, JToken> previous =
null;
298 string preferencesPath = PreferencesFileFor(profileDirectory);
300 if (
null == preferencesPath)
301 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No chrome Preferences file under [{profileDirectory}], so prompt for download and open pdf externally cannot be set for this profile.",
null, GPALObjectType.None);
305 JObject preferences = JObject.Parse(File.ReadAllText(preferencesPath));
307 previous =
new Dictionary<string, JToken>();
308 foreach (
string key
in downloadPreferenceKeys)
309 previous[key] = preferences.SelectToken(key);
311 SetByPath(preferences,
"download.prompt_for_download", promptForDownload);
312 SetByPath(preferences,
"plugins.always_open_pdf_externally", openPDFExternally);
316 SetByPath(preferences,
"browser.show_hub_popup_on_download_start",
false);
322 SetByPath(preferences,
"profile.exit_type",
"Normal");
324 if (
false ==
string.IsNullOrEmpty(downloadDirectory))
325 SetByPath(preferences,
"download.default_directory", downloadDirectory);
327 File.WriteAllText(preferencesPath, JsonConvert.SerializeObject(preferences));
332 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Profile [{preferencesPath}] prompt for download [{previous["download.prompt_for_download
"]?.ToString() ?? "unset
"}] -> [{promptForDownload}], open pdf externally [{previous["plugins.always_open_pdf_externally
"]?.ToString() ?? "unset
"}] -> [{openPDFExternally}], download directory [{previous["download.default_directory
"]?.ToString() ?? "unset
"}] -> [{downloadDirectory ?? "left alone
"}]",
null, GPALObjectType.None);
337 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not write download preferences into [{preferencesPath}]",
null, GPALObjectType.None, ex);
352 internal static void ApplyImagePreference(
string profileDirectory,
bool loadImages)
354 string preferencesPath = PreferencesFileFor(profileDirectory);
356 if (
null == preferencesPath)
357 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No chrome Preferences file under [{profileDirectory}], so load images cannot be set for this profile.",
null, GPALObjectType.None);
361 JObject preferences = JObject.Parse(File.ReadAllText(preferencesPath));
363 SetByPath(preferences,
"profile.default_content_setting_values.images",
true == loadImages ? 1 : 2);
365 File.WriteAllText(preferencesPath, JsonConvert.SerializeObject(preferences));
367 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Profile [{preferencesPath}] load images [{loadImages}]",
null, GPALObjectType.None);
371 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not write the load images preference into [{preferencesPath}]",
null, GPALObjectType.None, ex);
381 internal static void ApplyAdditionalPreferences(
string profileDirectory, Dictionary<string, object> additionalPrefs)
383 string preferencesPath = PreferencesFileFor(profileDirectory);
385 if (
null == preferencesPath)
386 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No chrome Preferences file under [{profileDirectory}], so the additional preferences cannot be set for this profile.",
null, GPALObjectType.None);
390 JObject preferences = JObject.Parse(File.ReadAllText(preferencesPath));
392 foreach (KeyValuePair<string, object> pref
in additionalPrefs)
393 SetByPath(preferences, pref.Key,
null == pref.Value ? JValue.CreateNull() : JToken.FromObject(pref.Value));
395 File.WriteAllText(preferencesPath, JsonConvert.SerializeObject(preferences));
397 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Profile [{preferencesPath}] took [{additionalPrefs.Count}] additional preference(s): [{string.Join(",
", additionalPrefs.Keys)}]",
null, GPALObjectType.None);
401 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not write the additional preferences into [{preferencesPath}]",
null, GPALObjectType.None, ex);
411 internal static void RestoreDownloadPreferences(
string profileDirectory, Dictionary<string, JToken> previous)
413 string preferencesPath =
null == previous ? null : PreferencesFileFor(profileDirectory);
415 if (
null == preferencesPath)
420 JObject preferences = JObject.Parse(File.ReadAllText(preferencesPath));
422 foreach (
string key
in downloadPreferenceKeys)
423 if (
null == previous[key])
424 preferences.SelectToken(key)?.Parent?.Remove();
426 SetByPath(preferences, key, previous[key]);
428 File.WriteAllText(preferencesPath, JsonConvert.SerializeObject(preferences));
432 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Profile [{preferencesPath}] put back, prompt for download [{previous["download.prompt_for_download
"]?.ToString() ?? "unset
"}], download directory [{previous["download.default_directory
"]?.ToString() ?? "unset
"}]",
null, GPALObjectType.None);
436 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Could not put the download preferences back in [{preferencesPath}]",
null, GPALObjectType.None, ex);
440 internal static void RemoveTempUserProfile(
string profileDirectory)
442 Exception innerEx =
null;
443 if (!Directory.Exists(profileDirectory))
449 for (
int i = 0; i < 10; i++)
453 if (
true == DeleteWhenNothingIsUsingIt(profileDirectory))
455 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $
"Deleted temporary user profile at [{profileDirectory}]");
459 System.Threading.Thread.Sleep(500);
464 System.Threading.Thread.Sleep(500);
465 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $
"Failed to delete temporary user profile at [{profileDirectory}][{innerEx.Message}]",
null, GPALObjectType.None, innerEx);
470 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, $
"Still in use after 10 tries, leaving temporary user profile at [{profileDirectory}] for the next run to clear");