From 0359b86174f496b0dac36f82a6b11c6f69fefdfb Mon Sep 17 00:00:00 2001 From: Parnic Date: Sun, 1 Dec 2024 11:13:56 -0600 Subject: [PATCH] Add automatic input downloading Since we no longer include input files with the solution, per AoC guidelines, this will enable other users to use this application (after specifying their session token) without manually grabbing all the appropriate download files. --- .gitignore | 1 + advent-of-code-2024.csproj | 10 ---- src/Logger.cs | 12 +++++ src/Util/DotEnv.cs | 65 ++++++++++++++++++++++ src/Util/Parsing.cs | 11 +++- src/Util/RetrieveInput.cs | 19 +++++++ src/main.cs | 108 ++++++++++++++++++++++++++----------- 7 files changed, 184 insertions(+), 42 deletions(-) create mode 100644 src/Util/DotEnv.cs create mode 100644 src/Util/RetrieveInput.cs diff --git a/.gitignore b/.gitignore index fd301fc..22b06e1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /obj/ *.exe /inputs/ +.env diff --git a/advent-of-code-2024.csproj b/advent-of-code-2024.csproj index 32483ca..9c37257 100644 --- a/advent-of-code-2024.csproj +++ b/advent-of-code-2024.csproj @@ -23,16 +23,6 @@ 1701;1702;8981 - - - - - - - - - - diff --git a/src/Logger.cs b/src/Logger.cs index 1181d7a..031a50a 100644 --- a/src/Logger.cs +++ b/src/Logger.cs @@ -50,12 +50,24 @@ internal class Logger Debug.WriteLine(StripColorCodes(msg)); } + public static void LogErrorLine(string msg) + { + Console.Error.WriteLine(InsertColorCodes(msg)); + Debug.WriteLine(StripColorCodes(msg)); + } + public static void Log(string msg) { Console.Write(InsertColorCodes(msg)); Debug.Write(StripColorCodes(msg)); } + public static void LogError(string msg) + { + Console.Error.Write(InsertColorCodes(msg)); + Debug.Write(StripColorCodes(msg)); + } + private static string InsertColorCodes(string msg) { foreach (var code in colorCodes) diff --git a/src/Util/DotEnv.cs b/src/Util/DotEnv.cs new file mode 100644 index 0000000..35b6285 --- /dev/null +++ b/src/Util/DotEnv.cs @@ -0,0 +1,65 @@ +namespace aoc2024.Util; + +internal static class DotEnv +{ + internal static string GetDotEnvContents(string fromPath = "") + { + if (string.IsNullOrEmpty(fromPath)) + { + fromPath = Directory.GetCurrentDirectory(); + } + + try + { + var dir = new DirectoryInfo(fromPath); + while (dir != null) + { + var dotEnv = Path.Combine(dir.FullName, ".env"); + if (File.Exists(dotEnv)) + { + return dotEnv; + } + + dir = dir.Parent; + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Exception searching for .env path from {fromPath}: {ex}"); + } + + return ""; + } + + internal static bool SetEnvironment(string fromPath = "") + { + var dotEnv = GetDotEnvContents(fromPath); + if (string.IsNullOrEmpty(dotEnv)) + { + return false; + } + + var lines = File.ReadAllLines(dotEnv); + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + + if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#')) + { + continue; + } + + var parts = line.Split('='); + if (parts.Length != 2) + { + Logger.LogErrorLine($"DotEnv file {dotEnv} line {i + 1} does not match expected `key=value` format. Line: {line}"); + continue; + } + + System.Diagnostics.Debug.WriteLine($"Setting environment variable `{parts[0]}` = `{parts[1]}`"); + Environment.SetEnvironmentVariable(parts[0], parts[1]); + } + + return true; + } +} diff --git a/src/Util/Parsing.cs b/src/Util/Parsing.cs index d47c0f8..12dd0ca 100644 --- a/src/Util/Parsing.cs +++ b/src/Util/Parsing.cs @@ -44,7 +44,7 @@ public static class Parsing } } - var filename = $"inputs/{inputName}.txt"; + var filename = Path.Combine("inputs", $"{inputName}.txt"); if (File.Exists(filename)) { if (Directory.Exists(Path.GetDirectoryName(filename)!) && File.Exists(filename)) @@ -63,7 +63,14 @@ public static class Parsing // accessible at runtime. instead, we assume Logger is also part of the "default namespace" var resourceName = $"{typeof(Logger).Namespace}.inputs.{inputName}.txt"; using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - using StreamReader reader = new(stream!); + if (stream == null) + { + Logger.LogErrorLine($"Unable to find file or resource matching requested input {inputName}."); + Logger.LogErrorLine("Do you have a .env file with an AOC_SESSION=... line containing your session cookie?"); + return; + } + + using StreamReader reader = new(stream); while (reader.ReadLine() is { } readLine) { processor(readLine); diff --git a/src/Util/RetrieveInput.cs b/src/Util/RetrieveInput.cs new file mode 100644 index 0000000..4c1e44c --- /dev/null +++ b/src/Util/RetrieveInput.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace aoc2024.Util; + +internal static class RetrieveInput +{ + internal static async Task Get(string session, string year, int day) + { + var cookies = new CookieContainer(); + cookies.Add(new Uri("https://adventofcode.com"), new Cookie("session", session)); + + using var handler = new HttpClientHandler() { CookieContainer = cookies }; + using var client = new HttpClient(handler); + + var inputContents = await client.GetStringAsync($"https://adventofcode.com/{year}/day/{day}/input"); + Directory.CreateDirectory("inputs"); + File.WriteAllText(Path.Combine("inputs", $"{day.ToString().PadLeft(2, '0')}.txt"), inputContents); + } +} diff --git a/src/main.cs b/src/main.cs index 38bddcb..d64fe15 100644 --- a/src/main.cs +++ b/src/main.cs @@ -39,40 +39,88 @@ if (runPart1 != null || runPart2 != null) if (runAll) { + desiredDays.Clear(); foreach (var type in types) { - using var day = (Day)Activator.CreateInstance(type)!; - day.Go(runPart1 ?? true, runPart2 ?? true); + desiredDays.Add(type.Name[^2..]); + } +} + +if (desiredDays.Count == 0) +{ + desiredDays.Add(""); +} + +var getDayNumFromArg = (string arg) => +{ + if (string.IsNullOrEmpty(arg)) + { + arg = types.Last().ToString()[^2..]; + } + + return int.Parse(arg); +}; + +var getDayInstanceFromArg = (string arg) => +{ + var num = getDayNumFromArg(arg); + var typ = types.FirstOrDefault(x => x.Name == $"Day{num.ToString().PadLeft(2, '0')}"); + if (typ == null) + { + return null; + } + + var day = (Day?) Activator.CreateInstance(typ); + return day; +}; + +aoc2024.Util.DotEnv.SetEnvironment(); +var sessionVal = Environment.GetEnvironmentVariable("AOC_SESSION"); +if (!string.IsNullOrEmpty(sessionVal)) +{ + var year = Environment.GetEnvironmentVariable("AOC_YEAR"); + if (string.IsNullOrEmpty(year)) + { + // was going to use the current year, but this solution is specifically designed for a certain year, so using that makes more sense. + // don't forget your find/replace when copying this code for a new aoc year! + //year = DateTime.Now.Year.ToString(); + year = "2024"; + System.Diagnostics.Debug.WriteLine($"No AOC_YEAR env var defined or set in .env file, so assuming year {year}"); + } + + foreach (var day in desiredDays) + { + var dayNum = getDayNumFromArg(day); + if (!File.Exists(Path.Combine("inputs", $"{dayNum.ToString().PadLeft(2, '0')}.txt"))) + { + Logger.Log($"Downloading input for day {dayNum}..."); + try + { + await aoc2024.Util.RetrieveInput.Get(sessionVal, year, dayNum); + Logger.LogLine("done!"); + } + catch (Exception ex) + { + Logger.LogLine($"failed! {ex}"); + } + + Logger.LogLine(""); + } } } else { - if (desiredDays.Count == 0) - { - desiredDays.Add(""); - } - - foreach (var desiredDay in desiredDays) - { - Day? day = null; - if (string.IsNullOrEmpty(desiredDay)) - { - day = (Day) Activator.CreateInstance(types.Last())!; - } - else - { - var type = types.FirstOrDefault(x => x.Name == $"Day{desiredDay.PadLeft(2, '0')}"); - if (type == null) - { - Logger.LogLine($"Unknown day {desiredDay}"); - } - else - { - day = (Day?) Activator.CreateInstance(type); - } - } - - day?.Go(runPart1 ?? true, runPart2 ?? true); - day?.Dispose(); - } + System.Diagnostics.Debug.WriteLine("No AOC_SESSION env var defined or set in .env file, so automatic input downloading not available."); +} + +foreach (var desiredDay in desiredDays) +{ + Day? day = getDayInstanceFromArg(desiredDay); + if (day == null) + { + Logger.LogLine($"Unknown day {desiredDay}"); + } + + day?.Go(runPart1 ?? true, runPart2 ?? true); + day?.Dispose(); }