2 Commits

Author SHA1 Message Date
Frog ee19990b5b Logging Infrastructure / Normalize Paths
- Added base logging infrastructure
- Create scan log during library/game scan in %ProgramData%\CreamInstaller\scan.log
- Normalize Steam library paths (libraryfolders.vdf) using Path.GetFullPath + ResolvePath to handle slashes, casing, and drive changes
- Diagnostics.ResolvePath: wrap GetFileSystemInfos in try/catch and guard against empty results to prevent IndexOutOfRangeException (May assist with issues on slow or intermittently accessible external drives)
2026-03-24 00:23:08 -07:00
Frog 39097c27ef Add Dev CI Builds
- Added CI action workflow for dev branch
2026-03-23 23:55:06 -07:00
5 changed files with 155 additions and 10 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Dev CI Builds
on:
push:
branches:
- dev
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore CreamInstaller.sln
- name: Build Release
run: dotnet build CreamInstaller.sln --configuration Release --no-restore
- name: Publish single-file
run: dotnet publish CreamInstaller.sln -c Release -r win-x64 -p:PublishSingleFile=true --self-contained true --output ./publish
- name: Set short commit SHA and branch name
id: vars
run: |
$shortSha = $env:GITHUB_SHA.Substring(0,7)
$branch = $env:GITHUB_REF_NAME
Write-Output "shortSha=$shortSha" >> $env:GITHUB_ENV
Write-Output "branch=$branch" >> $env:GITHUB_ENV
shell: pwsh
- name: Rename EXE with branch and short commit SHA
run: |
Rename-Item -Path ./publish/CreamInstaller.exe -NewName "CreamInstaller-CI-$env:branch-$env:shortSha.exe"
shell: pwsh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: CreamInstaller-CI-${{ env.branch }}-${{ env.shortSha }}
path: ./publish/CreamInstaller-CI-${{ env.branch }}-${{ env.shortSha }}.exe
+2
View File
@@ -569,6 +569,8 @@ internal sealed partial class SelectForm : CustomForm
progressLabel.Text = "Waiting for user to select which programs/games to scan . . .";
ShowProgressBar();
await ProgramData.Setup(this);
ProgramData.ClearLog();
ProgramData.Log($"[Scan] CreamInstaller {Program.Version} — scan started at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
bool scan = forceScan;
if (!scan && (programsToScan is null || programsToScan.Count < 1 || forceProvideChoices))
{
+59 -8
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CreamInstaller.Utility;
@@ -28,16 +29,19 @@ internal static class SteamLibrary
{
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
HashSet<string> gameLibraryDirectories = await GetLibraryDirectories();
ProgramData.Log($"[Steam] Found {gameLibraryDirectories.Count} library folder(s).");
foreach (string libraryDirectory in gameLibraryDirectories)
{
if (Program.Canceled)
return games;
ProgramData.Log($"[Steam] Scanning library: {libraryDirectory}");
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in (await
GetGamesFromLibraryDirectory(
libraryDirectory)).Where(game => games.All(_game => _game.appId != game.appId)))
games.Add(game);
}
ProgramData.Log($"[Steam] Total games detected: {games.Count}");
return games;
});
@@ -47,13 +51,21 @@ internal static class SteamLibrary
{
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
if (Program.Canceled || !libraryDirectory.DirectoryExists())
{
ProgramData.Log($"[Steam] Skipping library (not found or canceled): {libraryDirectory}");
return games;
}
foreach (string file in libraryDirectory.EnumerateDirectory("*.acf"))
{
if (Program.Canceled)
return games;
if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result))
{
ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}");
continue;
}
string appId = result.Value.GetChild("appid")?.ToString();
string installdir = result.Value.GetChild("installdir")?.ToString();
string name = result.Value.GetChild("name")?.ToString();
@@ -61,11 +73,23 @@ internal static class SteamLibrary
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) ||
string.IsNullOrWhiteSpace(name)
|| string.IsNullOrWhiteSpace(buildId))
{
ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}");
continue;
string gameDirectory = (libraryDirectory + @"\common\" + installdir).ResolvePath();
if (gameDirectory is null || !int.TryParse(appId, out int _) ||
!int.TryParse(buildId, out int buildIdInt) || games.Any(g => g.appId == appId))
}
string rawGameDirectory = libraryDirectory + @"\common\" + installdir;
string gameDirectory = rawGameDirectory.ResolvePath();
if (gameDirectory is null)
{
ProgramData.Log($"[Steam] Game directory not found (drive may be slow or disconnected): {rawGameDirectory} | App: {name} ({appId})");
continue;
}
if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt) ||
games.Any(g => g.appId == appId))
continue;
VToken userConfig = result.Value.GetChild("UserConfig");
string branch = userConfig?.GetChild("BetaKey")?.ToString();
branch ??= userConfig?.GetChild("betakey")?.ToString();
@@ -78,6 +102,8 @@ internal static class SteamLibrary
if (string.IsNullOrWhiteSpace(branch))
branch = "public";
ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {gameDirectory}");
games.Add((appId, name, branch, buildIdInt, gameDirectory));
}
@@ -92,25 +118,50 @@ internal static class SteamLibrary
return libraryDirectories;
string steamInstallPath = InstallPath;
if (steamInstallPath == null || !steamInstallPath.DirectoryExists())
{
ProgramData.Log($"[Steam] Steam install path not found or inaccessible: {steamInstallPath ?? "(null)"}");
return libraryDirectories;
}
string libraryFolder = steamInstallPath + @"\steamapps";
if (!libraryFolder.DirectoryExists())
{
ProgramData.Log($"[Steam] Default steamapps folder not found: {libraryFolder}");
return libraryDirectories;
}
_ = libraryDirectories.Add(libraryFolder);
ProgramData.Log($"[Steam] Default library folder: {libraryFolder}");
string libraryFolders = libraryFolder + @"\libraryfolders.vdf";
if (!libraryFolders.FileExists() ||
!ValveDataFile.TryDeserialize(libraryFolders.ReadFile(), out VProperty result))
{
ProgramData.Log($"[Steam] libraryfolders.vdf not found or failed to parse: {libraryFolders}");
return libraryDirectories;
}
foreach (VToken vToken in result.Value.Where(p =>
p is VProperty property && int.TryParse(property.Key, out int _)))
{
VProperty property = (VProperty)vToken;
string path = property.Value.GetChild("path")?.ToString();
if (string.IsNullOrWhiteSpace(path))
string rawPath = property.Value.GetChild("path")?.ToString();
if (string.IsNullOrWhiteSpace(rawPath))
continue;
path += @"\steamapps";
if (path.DirectoryExists())
_ = libraryDirectories.Add(path);
// Normalize the path from VDF (may use forward slashes or wrong casing)
string normalizedPath = Path.GetFullPath(rawPath);
string steamappsPath = normalizedPath + @"\steamapps";
string resolvedPath = steamappsPath.ResolvePath();
if (resolvedPath is null)
{
ProgramData.Log($"[Steam] External library not accessible (drive may be disconnected or letter changed): {steamappsPath}");
continue;
}
if (libraryDirectories.Add(resolvedPath))
ProgramData.Log($"[Steam] Additional library folder found: {resolvedPath}");
}
return libraryDirectories;
+11 -2
View File
@@ -54,7 +54,16 @@ internal static class Diagnostics
if (info.Parent is null)
return info.Name.ToUpperInvariant();
string parent = ResolvePath(info.Parent.FullName);
string name = info.Parent.GetFileSystemInfos(info.Name)[0].Name;
return parent is null ? name : Path.Combine(parent, name);
try
{
FileSystemInfo[] infos = info.Parent.GetFileSystemInfos(info.Name);
string name = infos.Length > 0 ? infos[0].Name : info.Name;
return parent is null ? name : Path.Combine(parent, name);
}
catch
{
// Fall back to the raw name if the filesystem call fails (e.g. on a slow external drive)
return parent is null ? info.Name : Path.Combine(parent, info.Name);
}
}
}
+34
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
@@ -28,6 +30,38 @@ internal static class ProgramData
private static readonly string DlcChoicesPath = DirectoryPath + @"\dlc.json";
private static readonly string KoaloaderProxyChoicesPath = DirectoryPath + @"\proxies.json";
internal static readonly string LogPath = DirectoryPath + @"\scan.log";
private static readonly object LogLock = new();
internal static void Log(string message)
{
try
{
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
lock (LogLock)
File.AppendAllText(LogPath, entry, Encoding.UTF8);
}
catch
{
// ignored — logging must never crash the application
}
}
internal static void ClearLog()
{
try
{
if (File.Exists(LogPath))
File.Delete(LogPath);
}
catch
{
// ignored
}
}
internal static async Task Setup(Form form = null)
=> await Task.Run(() =>
{