src/SharedUpdateState.cs
Rama main · réplica de lectura del repositorio original.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Windows.Forms;
namespace Clipman
{
internal sealed class SharedUpdateState
{
public int Schema { get; set; }
public string Version { get; set; }
public long BuildStampUtcMs { get; set; }
public string ExeSha256 { get; set; }
public string UpdatedByMachine { get; set; }
public long UpdatedAtUtcMs { get; set; }
public long CloseRequestUntilUtcMs { get; set; }
public string CloseRequestId { get; set; }
public string CloseRequestedByMachine { get; set; }
public SharedUpdateState()
{
Schema = 1;
Version = string.Empty;
ExeSha256 = string.Empty;
UpdatedByMachine = string.Empty;
CloseRequestId = string.Empty;
CloseRequestedByMachine = string.Empty;
}
}
internal static class SharedUpdateStateStore
{
private const string StateFileName = "clipman-shared-state.json";
public static string StatePath(string settingsDirectory)
{
return Path.Combine(settingsDirectory, StateFileName);
}
public static SharedUpdateState Load(string settingsDirectory)
{
if (string.IsNullOrWhiteSpace(settingsDirectory)) return new SharedUpdateState();
try
{
ResolveStateConflicts(settingsDirectory);
return TryLoad(StatePath(settingsDirectory)) ?? new SharedUpdateState();
}
catch
{
return new SharedUpdateState();
}
}
public static bool CanCoordinateExecutable(string settingsDirectory, string executablePath)
{
if (string.IsNullOrWhiteSpace(settingsDirectory) || string.IsNullOrWhiteSpace(executablePath)) return false;
try
{
var executableDirectory = Path.GetDirectoryName(Path.GetFullPath(executablePath));
if (string.IsNullOrWhiteSpace(executableDirectory)) return false;
var expectedSettingsDirectory = Path.Combine(executableDirectory, "Settings");
return string.Equals(
NormalizeDirectory(settingsDirectory),
NormalizeDirectory(expectedSettingsDirectory),
StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
public static void PublishCurrentBuild(string settingsDirectory, string executablePath)
{
if (!CanCoordinateExecutable(settingsDirectory, executablePath)) return;
try
{
Directory.CreateDirectory(settingsDirectory);
var existing = Load(settingsDirectory);
if (existing != null && existing.BuildStampUtcMs > BuildInfo.BuildStampUtcMs)
{
return;
}
JsonUtil.SaveAtomic(StatePath(settingsDirectory), CurrentState(executablePath));
}
catch
{
}
}
public static bool ShouldRestartForState(string settingsDirectory, string executablePath, out SharedUpdateState state, out string reason)
{
state = null;
reason = string.Empty;
if (!CanCoordinateExecutable(settingsDirectory, executablePath)) return false;
try
{
state = Load(settingsDirectory);
if (state == null || state.BuildStampUtcMs <= BuildInfo.BuildStampUtcMs)
{
return false;
}
var expectedHash = (state.ExeSha256 ?? string.Empty).Trim();
if (expectedHash.Length == 0)
{
reason = "shared state has newer build stamp";
return true;
}
var diskHash = HashFile(executablePath);
if (string.Equals(diskHash, expectedHash, StringComparison.OrdinalIgnoreCase))
{
reason = "shared state and executable hash indicate a newer build is on disk";
return true;
}
reason = "shared state is newer, but the updated executable has not synced to this machine yet";
return false;
}
catch
{
return false;
}
}
public static void PublishCloseRequest(string settingsDirectory, string executablePath, int seconds)
{
if (!CanCoordinateExecutable(settingsDirectory, executablePath)) return;
try
{
Directory.CreateDirectory(settingsDirectory);
var state = Load(settingsDirectory) ?? new SharedUpdateState();
state.CloseRequestUntilUtcMs = TimeUtil.NowUnixMs() + Math.Max(10, seconds) * 1000L;
state.CloseRequestId = Guid.NewGuid().ToString("N");
state.CloseRequestedByMachine = Environment.MachineName ?? string.Empty;
if (state.BuildStampUtcMs <= 0)
{
state.BuildStampUtcMs = BuildInfo.BuildStampUtcMs;
}
if (string.IsNullOrWhiteSpace(state.Version))
{
state.Version = AppVersion();
}
if (string.IsNullOrWhiteSpace(state.ExeSha256))
{
state.ExeSha256 = CurrentExeHash(executablePath);
}
JsonUtil.SaveAtomic(StatePath(settingsDirectory), state);
}
catch
{
}
}
public static bool HasActiveCloseRequest(string settingsDirectory, string executablePath, string lastHandledRequestId, out SharedUpdateState state)
{
state = null;
if (!CanCoordinateExecutable(settingsDirectory, executablePath)) return false;
try
{
state = Load(settingsDirectory);
if (state == null) return false;
if (state.CloseRequestUntilUtcMs <= TimeUtil.NowUnixMs()) return false;
if (string.IsNullOrWhiteSpace(state.CloseRequestId)) return false;
if (string.Equals(state.CloseRequestId, lastHandledRequestId, StringComparison.OrdinalIgnoreCase)) return false;
if (string.Equals(state.CloseRequestedByMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase)) return false;
return true;
}
catch
{
return false;
}
}
public static bool IsNewerStateFromAnotherMachine(SharedUpdateState state)
{
if (state == null) return false;
if (state.BuildStampUtcMs <= BuildInfo.BuildStampUtcMs) return false;
if (string.Equals(state.UpdatedByMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase)) return false;
return true;
}
public static string CurrentExeHash()
{
return CurrentExeHash(Application.ExecutablePath);
}
private static string CurrentExeHash(string executablePath)
{
try
{
return HashFile(executablePath);
}
catch
{
return string.Empty;
}
}
private static SharedUpdateState CurrentState(string executablePath)
{
return new SharedUpdateState
{
Schema = 1,
Version = AppVersion(),
BuildStampUtcMs = BuildInfo.BuildStampUtcMs,
ExeSha256 = CurrentExeHash(executablePath),
UpdatedByMachine = Environment.MachineName ?? string.Empty,
UpdatedAtUtcMs = TimeUtil.NowUnixMs()
};
}
private static void ResolveStateConflicts(string settingsDirectory)
{
try
{
var canonicalPath = StatePath(settingsDirectory);
var conflicts = Directory.GetFiles(settingsDirectory, "*.json")
.Where(path => IsStateConflict(path, canonicalPath))
.ToList();
if (conflicts.Count == 0) return;
var candidates = new List<Tuple<string, SharedUpdateState>>();
var canonical = TryLoad(canonicalPath);
if (canonical != null) candidates.Add(Tuple.Create(canonicalPath, canonical));
foreach (var conflict in conflicts)
{
var candidate = TryLoad(conflict);
if (candidate != null) candidates.Add(Tuple.Create(conflict, candidate));
}
var newest = candidates
.Where(item => item.Item2 != null)
.OrderByDescending(item => item.Item2.BuildStampUtcMs)
.ThenByDescending(item => item.Item2.UpdatedAtUtcMs)
.FirstOrDefault();
if (newest != null && newest.Item2 != null)
{
JsonUtil.SaveAtomic(canonicalPath, newest.Item2);
}
if (newest == null || newest.Item2 == null) return;
foreach (var conflict in conflicts)
{
TryDelete(conflict);
}
}
catch
{
}
}
private static bool IsStateConflict(string path, string canonicalPath)
{
if (string.Equals(path, canonicalPath, StringComparison.OrdinalIgnoreCase)) return false;
var name = Path.GetFileNameWithoutExtension(path);
if (string.IsNullOrWhiteSpace(name) ||
!name.StartsWith("clipman-shared-state", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var suffix = name.Substring("clipman-shared-state".Length).Trim().ToLowerInvariant();
if (suffix.Length == 0) return false;
return suffix.Contains("conflicted copy") ||
suffix.StartsWith(".sync-conflict-", StringComparison.Ordinal) ||
suffix.Contains("[conflict]") ||
suffix.Contains(" conflict") ||
suffix.StartsWith("_conf(") ||
suffix.StartsWith("-") ||
suffix.StartsWith("(");
}
private static SharedUpdateState TryLoad(string path)
{
try
{
return File.Exists(path) ? JsonUtil.Load<SharedUpdateState>(path) : null;
}
catch
{
return null;
}
}
private static string NormalizeDirectory(string path)
{
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
private static string HashFile(string path)
{
using (var sha = SHA256.Create())
using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
return BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant();
}
}
private static string AppVersion()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
return version == null ? "1.1.0" : version.Major + "." + version.Minor + "." + version.Build;
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch
{
}
}
}
}