Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/c#/GeneralUpdate.Bowl/Strategies/LinuxBowlStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,10 @@ private static string DetectDistro()

private static void EnsureDirectory(string path)
{
if (Directory.Exists(path))
Directory.Delete(path, recursive: true);
Directory.CreateDirectory(path);
// Create the fail directory if it does not yet exist.
// Do NOT delete an existing directory — that would destroy
// crash diagnostics from previous surveillance sessions.
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
}
14 changes: 9 additions & 5 deletions src/c#/GeneralUpdate.Bowl/Strategies/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static async Task<ProcessExitResult> RunAsync(
var outputLines = new List<string>();

using var process = new Process { StartInfo = startInfo };
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);

process.OutputDataReceived += (_, e) =>
Expand Down Expand Up @@ -61,14 +62,17 @@ public static async Task<ProcessExitResult> RunAsync(
process.BeginOutputReadLine();
process.BeginErrorReadLine();

// Wait for exit or timeout/cancellation
var completedTask = await Task.WhenAny(
tcs.Task,
Task.Delay(timeoutMs, ct)
);
// Wait for exit or timeout/cancellation.
// Cancel the delay when the process exits first to avoid a timer leak.
var delayTask = Task.Delay(timeoutMs, timeoutCts.Token);
var completedTask = await Task.WhenAny(tcs.Task, delayTask);

// Cancel the opposing task so timers/resources are reclaimed promptly.
timeoutCts.Cancel();

if (completedTask == tcs.Task)
{
try { await delayTask; } catch (OperationCanceledException) { /* cancelled — expected */ }
var exitCode = await tcs.Task;
GeneralTracer.Info($"ProcessRunner.RunAsync: process exited, ExitCode={exitCode}");
// Snapshot output under lock to avoid race with in-flight handlers
Expand Down
8 changes: 5 additions & 3 deletions src/c#/GeneralUpdate.Bowl/Strategies/WindowsBowlStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ public Task PostProcessAsync(in BowlContext context,

private static void EnsureDirectory(string path)
{
if (Directory.Exists(path))
StorageHelper.DeleteDirectory(path);
Directory.CreateDirectory(path);
// Create the fail directory if it does not yet exist.
// Do NOT delete an existing directory — that would destroy
// crash diagnostics from previous surveillance sessions.
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
}
8 changes: 7 additions & 1 deletion src/c#/GeneralUpdate.Core/Event/EventManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,15 @@ public void Dispatch<TEventArgs>(object sender, TEventArgs eventArgs) where TEve
var type = typeof(Action<object, TEventArgs>);
if (_dicDelegates.TryGetValue(type, out var existingDelegate))
{
// Snapshot the delegate invocation list to avoid a race with
// concurrent AddListener / RemoveListener mutating the delegate
// while we enumerate it. The ConcurrentDictionary protects the
// dictionary structure but not the Delegate value itself.
var invocationList = existingDelegate.GetInvocationList();

// Invoke each handler individually so one handler's exception
// doesn't prevent others from being called.
foreach (var handler in existingDelegate.GetInvocationList())
foreach (var handler in invocationList)
{
try
{
Expand Down
3 changes: 1 addition & 2 deletions src/c#/GeneralUpdate.Core/FileSystem/FileNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,7 @@ public FileNode SearchParent(long id)
public override bool Equals(object obj)
{
if (obj == null) return false;
var tempNode = obj as FileNode;
if (tempNode == null) throw new ArgumentException(nameof(tempNode));
if (obj is not FileNode tempNode) return false;
return string.Equals(Hash, tempNode.Hash, StringComparison.OrdinalIgnoreCase) &&
string.Equals(Name, tempNode.Name, StringComparison.OrdinalIgnoreCase);
}
Expand Down
8 changes: 4 additions & 4 deletions src/c#/GeneralUpdate.Core/FileSystem/FileTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,23 +209,23 @@ public void Compare(FileNode node, FileNode node0, ref List<FileNode> nodes)
if (node != null && node.Left != null)
{
if (!node.Equals(node0) && node0 != null) nodes.Add(node0);
Compare(node.Left, node0.Left, ref nodes);
Compare(node.Left, node0?.Left, ref nodes);
}
else if (node0 != null && node0.Left != null)
{
nodes.Add(node0);
Compare(node.Left, node0.Left, ref nodes);
Compare(node?.Left, node0.Left, ref nodes);
}

if (node != null && node.Right != null)
{
if (!node.Equals(node0) && node0 != null) nodes.Add(node0);
Compare(node.Right, node0 == null ? null : node0.Right, ref nodes);
Compare(node.Right, node0?.Right, ref nodes);
}
else if (node0 != null && node0.Right != null)
{
nodes.Add(node0);
Compare(node == null ? null : node.Right, node0.Right, ref nodes);
Compare(node?.Right, node0.Right, ref nodes);
}
else if (node0 != null)
{
Expand Down
4 changes: 2 additions & 2 deletions src/c#/GeneralUpdate.Core/Pipeline/DiffPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public async Task CleanAsync(
}

int completed = 0;
var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism);
using var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism);

var tasks = differentFiles.Select(file => Task.Run(async () =>
{
Expand Down Expand Up @@ -342,7 +342,7 @@ public async Task DirtyAsync(
}

int completed = 0;
var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism);
using var semaphore = new SemaphoreSlim(_options.MaxDegreeOfParallelism);

var matchedPairs = new List<(FileInfo OldFile, FileInfo PatchFile)>();
foreach (var oldFile in oldFiles)
Expand Down
5 changes: 4 additions & 1 deletion src/c#/GeneralUpdate.Core/Strategy/ClientStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,10 @@ private bool CheckFail(string version)
var fail = Environments.GetEnvironmentVariable("UpgradeFail");
if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version))
return false;
return new Version(fail) >= new Version(version);
if (!Version.TryParse(fail, out var failVersion) ||
!Version.TryParse(version, out var versionParsed))
return false;
return failVersion >= versionParsed;
}

/// <summary>
Expand Down
12 changes: 8 additions & 4 deletions src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
Expand Down Expand Up @@ -258,15 +259,18 @@ private async Task ExecuteClientAsync()
: installPath;
var upgradeAppName = !string.IsNullOrWhiteSpace(_configInfo.UpdateAppName)
? _configInfo.UpdateAppName
: "GeneralUpdate.Upgrade.exe";
: RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "GeneralUpdate.Upgrade.exe"
: "GeneralUpdate.Upgrade";
var appPath = Path.Combine(upgradeDir, upgradeAppName);
GeneralTracer.Info($"[OssClient] Resolved upgrade path: {appPath}");

// List exe files in the directory to help diagnose missing file issues
// List executables in the directory to help diagnose missing file issues
try
{
var dirFiles = Directory.GetFiles(upgradeDir, "*.exe").Select(f => Path.GetFileName(f));
GeneralTracer.Info($"[OssClient] *.exe files in {upgradeDir}: [{string.Join(", ", dirFiles)}]");
var searchPattern = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "*.exe" : "*";
var dirFiles = Directory.GetFiles(upgradeDir, searchPattern).Select(f => Path.GetFileName(f));
GeneralTracer.Info($"[OssClient] Files in {upgradeDir}: [{string.Join(", ", dirFiles)}]");
}
catch (Exception ex)
{
Expand Down
39 changes: 21 additions & 18 deletions src/c#/GeneralUpdate.Core/Tracer/GeneralTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,30 @@ static GeneralTracer()

private static void InitializeFileListener()
{
//Ensure that log files are rotated on a daily basis
var today = DateTime.Now.ToString("yyyy-MM-dd");
if (today == _currentLogDate && _fileListener != null)
return;

if (_fileListener != null)
lock (_lockObj)
{
Trace.Listeners.Remove(_fileListener);
_fileListener.Flush();
_fileListener.Close();
_fileListener.Dispose();
}
// Ensure that log files are rotated on a daily basis
var today = DateTime.Now.ToString("yyyy-MM-dd");
if (today == _currentLogDate && _fileListener != null)
return;

var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
Directory.CreateDirectory(logDir);
if (_fileListener != null)
{
Trace.Listeners.Remove(_fileListener);
_fileListener.Flush();
_fileListener.Close();
_fileListener.Dispose();
}

var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
Directory.CreateDirectory(logDir);

var logFileName = Path.Combine(logDir, $"generalupdate-trace {today}.log");
_fileListener = new TextTraceListener(logFileName);

Trace.Listeners.Add(_fileListener);
_currentLogDate = today;
var logFileName = Path.Combine(logDir, $"generalupdate-trace {today}.log");
_fileListener = new TextTraceListener(logFileName);

Trace.Listeners.Add(_fileListener);
_currentLogDate = today;
}
}

public static void Debug(string message) => WriteTraceMessage(TraceLevel.Verbose, message);
Expand Down
22 changes: 17 additions & 5 deletions src/c#/GeneralUpdate.Differential/Differ/BsdiffDiffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,11 @@ await Task.Run(() =>
byte[] formatByte = ReadExactly(patchStream, 1);
byte candidate = formatByte[0];

if (candidate == BZip2FormatVersion || candidate == DeflateFormatVersion)
if (candidate == BZip2FormatVersion || candidate == DeflateFormatVersion
#if NET6_0_OR_GREATER
|| candidate == BrotliFormatVersion
#endif
)
Comment on lines +311 to +313
{
formatVersion = candidate;
actualHeaderSize = ExtendedHeaderSize;
Expand All @@ -333,6 +337,9 @@ await Task.Run(() =>
{
BZip2FormatVersion => new BZip2CompressionProvider(),
DeflateFormatVersion => new DeflateCompressionProvider(),
#if NET6_0_OR_GREATER
BrotliFormatVersion => new BrotliCompressionProvider(),
#endif
_ => throw new InvalidOperationException(
$"Unsupported patch compression format version: 0x{formatVersion:X2}")
};
Expand Down Expand Up @@ -437,6 +444,9 @@ await Task.Run(() =>

private const byte BZip2FormatVersion = 0x00;
private const byte DeflateFormatVersion = 0x01;
#if NET6_0_OR_GREATER
private const byte BrotliFormatVersion = 0x02;
#endif

private static FileStream OpenPatchStream(string patchPath)
{
Expand Down Expand Up @@ -477,17 +487,19 @@ private static int Search(int[] I, byte[] oldData, byte[] newData, int newOffset
{
if (end - start < 2)
{
int x = MatchLength(oldData, I[start], newData, newOffset);
int y = MatchLength(oldData, I[end], newData, newOffset);
// Guard against sentinel -1 values that Split() writes for singleton buckets.
// MatchLength with a negative index would throw IndexOutOfRangeException.
int x = I[start] >= 0 ? MatchLength(oldData, I[start], newData, newOffset) : 0;
int y = I[end] >= 0 ? MatchLength(oldData, I[end], newData, newOffset) : 0;

if (x > y)
{
pos = I[start];
pos = I[start] >= 0 ? I[start] : 0;
return x;
}
else
{
pos = I[end];
pos = I[end] >= 0 ? I[end] : 0;
return y;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,16 +436,20 @@ private static void ValidatePaths(string oldPath, string newPath, string patchPa
private static byte[] ReadFileWithBudget(string path, int maxBytes)
{
byte[] buffer = new byte[maxBytes];
int totalRead = 0;
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
int totalRead = 0;
while (totalRead < maxBytes)
{
int read = fs.Read(buffer, totalRead, maxBytes - totalRead);
if (read == 0) break;
totalRead += read;
}
}
// Trim to actual bytes read so callers using .Length see the real data,
// not zero-initialized tail bytes that would corrupt patch computation.
if (totalRead < maxBytes)
Array.Resize(ref buffer, totalRead);
return buffer;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ private static SemVerInfo ParseVersion(string version)

return new SemVerInfo
{
Major = int.Parse(match.Groups["major"].Value),
Minor = int.Parse(match.Groups["minor"].Value),
Patch = int.Parse(match.Groups["patch"].Value),
Major = long.Parse(match.Groups["major"].Value),
Minor = long.Parse(match.Groups["minor"].Value),
Patch = long.Parse(match.Groups["patch"].Value),
Prerelease = match.Groups["prerelease"].Value,
BuildMetadata = match.Groups["buildmetadata"].Value
};
Comment on lines 113 to 120
Expand All @@ -122,8 +122,8 @@ private static int ComparePrerelease(string pre1, string pre2)

for (int i = 0; i < minLength; i++)
{
var isNum1 = int.TryParse(parts1[i], out int num1);
var isNum2 = int.TryParse(parts2[i], out int num2);
var isNum1 = long.TryParse(parts1[i], out long num1);
var isNum2 = long.TryParse(parts2[i], out long num2);

if (isNum1 && isNum2)
{
Expand Down Expand Up @@ -152,9 +152,9 @@ private static int ComparePrerelease(string pre1, string pre2)

private class SemVerInfo
{
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public long Major { get; set; }
public long Minor { get; set; }
public long Patch { get; set; }
public string Prerelease { get; set; } = string.Empty;
public string BuildMetadata { get; set; } = string.Empty;
}
Expand Down
26 changes: 13 additions & 13 deletions src/c#/GeneralUpdate.Extension/Core/GeneralExtensionHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,21 @@ public async Task<bool> InstallExtensionAsync(string extensionPath, bool rollbac
throw new InvalidOperationException("Extension file must be a .zip file");
}

// Extract extension name from file name (e.g., "demo-extension_1.0.0.zip" -> "demo-extension")
var fileName = Path.GetFileNameWithoutExtension(extensionPath);
var extensionName = fileName;

// Try to parse extension name if it follows pattern "name_version"
var underscoreIndex = fileName.LastIndexOf('_');
if (underscoreIndex > 0)
{
extensionName = fileName.Substring(0, underscoreIndex);
}

// Invoke lifecycle hook: before install
if (_lifecycleHooks != null)
{
var tempMeta = new ExtensionMetadata { Id = extensionPath, Name = Path.GetFileNameWithoutExtension(extensionPath) };
var tempMeta = new ExtensionMetadata { Id = extensionName, Name = extensionName };
var canInstall = await _lifecycleHooks.OnBeforeInstallAsync(tempMeta, extensionPath);
if (!canInstall)
{
Expand All @@ -357,17 +368,6 @@ public async Task<bool> InstallExtensionAsync(string extensionPath, bool rollbac
}
}

// Extract extension name from file name (e.g., "demo-extension_1.0.0.zip" -> "demo-extension")
var fileName = Path.GetFileNameWithoutExtension(extensionPath);
var extensionName = fileName;

// Try to parse extension name if it follows pattern "name_version"
var underscoreIndex = fileName.LastIndexOf('_');
if (underscoreIndex > 0)
{
extensionName = fileName.Substring(0, underscoreIndex);
}

// Determine target installation directory for this extension
var targetExtensionDir = Path.Combine(_extensionsDirectory, extensionName);
extractedExtensionDir = targetExtensionDir;
Expand Down Expand Up @@ -419,7 +419,7 @@ public async Task<bool> InstallExtensionAsync(string extensionPath, bool rollbac
// Invoke lifecycle hook: after install
if (_lifecycleHooks != null)
{
var installedMeta = new ExtensionMetadata { Id = extensionPath, Name = extensionName };
var installedMeta = new ExtensionMetadata { Id = extensionName, Name = extensionName };
await _lifecycleHooks.OnAfterInstallAsync(installedMeta);
}

Expand Down
Loading
Loading