Ctrl K
ComunidadGuías…

src/UpdateService.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Web.Script.Serialization;
using System.Windows.Forms;

namespace Clipman
{
    internal static class UpdateService
    {
        public const string ProjectUrl = "https://github.com/OnjLouis/Clipman";

        private const string UserAgent = "Clipman updater";

        public static void CheckForUpdates(IWin32Window owner, string currentVersion, Action exitApp)
        {
            try
            {
                var releases = FetchReleases();
                var release = LatestWindowsRelease(releases);
                var latestVersionText = release == null ? string.Empty : (release.TagName ?? string.Empty).Trim().TrimStart('v', 'V');
                Version current;
                Version remote;
                if (string.IsNullOrWhiteSpace(latestVersionText) ||
                    !Version.TryParse(currentVersion, out current) ||
                    !Version.TryParse(latestVersionText, out remote))
                {
                    MessageBox.Show(owner, "Could not read the latest Clipman release version.", "Clipman Update", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    return;
                }

                if (remote <= current)
                {
                    MessageBox.Show(owner, "Clipman is up to date. Current version: " + currentVersion + ".", "Clipman Update", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    return;
                }

                ShowUpdateAvailableDialog(owner, release, releases, current, remote, exitApp);
            }
            catch (WebException ex)
            {
                MessageBox.Show(owner, "Could not check for updates. GitHub releases may not exist yet, or the network request failed." + Environment.NewLine + Environment.NewLine + ex.Message, "Clipman Update", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show(owner, ex.Message, "Clipman Update", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        public static void CheckForUpdatesAutomatic(IWin32Window owner, string currentVersion, Action exitApp, bool installSilently)
        {
            try
            {
                var releases = FetchReleases();
                var release = LatestWindowsRelease(releases);
                var latestVersionText = release == null ? string.Empty : (release.TagName ?? string.Empty).Trim().TrimStart('v', 'V');
                Version current;
                Version remote;
                if (string.IsNullOrWhiteSpace(latestVersionText) ||
                    !Version.TryParse(currentVersion, out current) ||
                    !Version.TryParse(latestVersionText, out remote) ||
                    remote <= current)
                {
                    return;
                }

                if (installSilently)
                {
                    var zipAsset = FindPortableZipAsset(release);
                    if (zipAsset == null || string.IsNullOrWhiteSpace(zipAsset.BrowserDownloadUrl)) return;
                    StartSelfUpdate(owner, zipAsset.BrowserDownloadUrl, exitApp, true);
                    return;
                }

                ShowUpdateAvailableDialog(owner, release, releases, current, remote, exitApp);
            }
            catch
            {
            }
        }

        public static void ShowVersionHistory(IWin32Window owner, string currentVersion)
        {
            try
            {
                var releases = FetchReleases();
                var release = LatestWindowsRelease(releases);
                var version = release == null ? currentVersion : (release.TagName ?? currentVersion).Trim().TrimStart('v', 'V');
                var releaseUrl = release == null || string.IsNullOrWhiteSpace(release.HtmlUrl) ? ProjectUrl + "/releases" : release.HtmlUrl;
                var notesText = FormatReleaseNotesForDialog(release == null ? string.Empty : release.Body, "No release notes were provided for this update.");

                using (var dialog = new Form())
                {
                    dialog.Text = "Version history - " + version;
                    dialog.StartPosition = FormStartPosition.CenterParent;
                    dialog.Width = 720;
                    dialog.Height = 520;
                    dialog.MinimizeBox = false;
                    dialog.MaximizeBox = false;
                    dialog.ShowIcon = false;
                    dialog.ShowInTaskbar = false;

                    var layout = new TableLayoutPanel
                    {
                        Dock = DockStyle.Fill,
                        ColumnCount = 1,
                        RowCount = 3,
                        Padding = new Padding(12)
                    };
                    layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
                    layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
                    layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));

                    layout.Controls.Add(new Label
                    {
                        AutoSize = true,
                        Dock = DockStyle.Top,
                        Text = "Latest release: " + version,
                        Padding = new Padding(0, 0, 0, 8)
                    }, 0, 0);

                    layout.Controls.Add(new TextBox
                    {
                        Dock = DockStyle.Fill,
                        Multiline = true,
                        ReadOnly = true,
                        ScrollBars = ScrollBars.Vertical,
                        Text = "Latest release: " + version + Environment.NewLine + Environment.NewLine + notesText,
                        AccessibleName = "Version history " + version
                    }, 0, 1);

                    var buttons = new FlowLayoutPanel
                    {
                        AutoSize = true,
                        Dock = DockStyle.Fill,
                        FlowDirection = FlowDirection.LeftToRight,
                        Padding = new Padding(0, 8, 0, 0)
                    };
                    var releaseButton = new Button { Text = "Open &release page", AutoSize = true };
                    releaseButton.Click += delegate { OpenUrl(releaseUrl); };
                    var allReleasesButton = new Button { Text = "Open &all releases", AutoSize = true };
                    allReleasesButton.Click += delegate { OpenUrl(ProjectUrl + "/releases"); };
                    var closeButton = new Button { Text = "&Close", DialogResult = DialogResult.OK, AutoSize = true };
                    buttons.Controls.Add(releaseButton);
                    buttons.Controls.Add(allReleasesButton);
                    buttons.Controls.Add(closeButton);

                    layout.Controls.Add(buttons, 0, 2);
                    dialog.Controls.Add(layout);
                    dialog.AcceptButton = closeButton;
                    dialog.CancelButton = closeButton;
                    dialog.ShowDialog(owner);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(owner, "Could not check version history. GitHub releases may not exist yet, or the network request failed." + Environment.NewLine + Environment.NewLine + ex.Message, "Version history", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

        public static void OpenProjectPage()
        {
            OpenUrl(ProjectUrl);
        }

        public static void OpenContactPage()
        {
            OpenUrl("https://onj.me/contact");
        }

        public static void OpenDonatePage()
        {
            OpenUrl("https://onj.me/donate");
        }

        private static void ShowUpdateAvailableDialog(IWin32Window owner, GitHubReleaseInfo release, IEnumerable<GitHubReleaseInfo> releases, Version current, Version remote, Action exitApp)
        {
            var latest = release == null ? remote.ToString() : (release.TagName ?? remote.ToString());
            var releaseUrl = release == null || string.IsNullOrWhiteSpace(release.HtmlUrl) ? ProjectUrl + "/releases" : release.HtmlUrl;
            var zipAsset = FindPortableZipAsset(release);
            var releaseNotes = BuildUpdateReleaseNotes(releases, current, remote);

            using (var dialog = new Form())
            {
                dialog.Text = "Clipman Update";
                dialog.StartPosition = FormStartPosition.CenterParent;
                dialog.Width = 720;
                dialog.Height = 520;
                dialog.MinimizeBox = false;
                dialog.MaximizeBox = false;
                dialog.ShowIcon = false;
                dialog.ShowInTaskbar = false;

                var layout = new TableLayoutPanel
                {
                    Dock = DockStyle.Fill,
                    ColumnCount = 1,
                    RowCount = 3,
                    Padding = new Padding(12)
                };
                layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
                layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
                layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));

                var header = new Label
                {
                    AutoSize = true,
                    Dock = DockStyle.Top,
                    Text = "Clipman " + latest + " is available.",
                    Padding = new Padding(0, 0, 0, 8)
                };

                var notes = new TextBox
                {
                    Dock = DockStyle.Fill,
                    Multiline = true,
                    ReadOnly = true,
                    ScrollBars = ScrollBars.Vertical,
                    Text = releaseNotes,
                    AccessibleName = "Release notes"
                };

                var buttons = new FlowLayoutPanel
                {
                    AutoSize = true,
                    Dock = DockStyle.Fill,
                    FlowDirection = FlowDirection.LeftToRight,
                    Padding = new Padding(0, 8, 0, 0)
                };

                var laterButton = new Button { Text = "&Later", DialogResult = DialogResult.Cancel, AutoSize = true };
                var releaseButton = new Button { Text = "Open &release page", AutoSize = true };
                releaseButton.Click += delegate { OpenUrl(releaseUrl); };

                if (zipAsset != null && !string.IsNullOrWhiteSpace(zipAsset.BrowserDownloadUrl))
                {
                    var installButton = new Button { Text = "&Download and install", AutoSize = true };
                    installButton.Click += delegate
                    {
                        dialog.DialogResult = DialogResult.OK;
                        dialog.Close();
                        StartSelfUpdate(owner, zipAsset.BrowserDownloadUrl, exitApp, false);
                    };
                    buttons.Controls.Add(installButton);
                    dialog.AcceptButton = installButton;
                }

                buttons.Controls.Add(releaseButton);
                buttons.Controls.Add(laterButton);

                dialog.CancelButton = laterButton;
                layout.Controls.Add(header, 0, 0);
                layout.Controls.Add(notes, 0, 1);
                layout.Controls.Add(buttons, 0, 2);
                dialog.Controls.Add(layout);
                dialog.ShowDialog(owner);
            }
        }

        private static void StartSelfUpdate(IWin32Window owner, string zipUrl, Action exitApp, bool silent)
        {
            if (!silent &&
                MessageBox.Show(
                        owner,
                        "Clipman will close, download the update, replace the files in this folder, and restart. Your Settings folder will be kept." + Environment.NewLine + Environment.NewLine + "Do you want to continue?",
                        "Clipman Update",
                        MessageBoxButtons.YesNo,
                        MessageBoxIcon.Question,
                        MessageBoxDefaultButton.Button2) != DialogResult.Yes)
            {
                return;
            }

            try
            {
                var appDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
                var exePath = Application.ExecutablePath;
                var updaterTempDir = GetUpdaterTempDirectory(appDir);
                var updaterRoot = Path.Combine(updaterTempDir, "ClipmanUpdater-" + Guid.NewGuid().ToString("N"));
                Directory.CreateDirectory(updaterRoot);
                var updaterExe = Path.Combine(updaterRoot, "Clipman Updater.exe");
                File.Copy(exePath, updaterExe, true);
                Process.Start(new ProcessStartInfo
                {
                    FileName = updaterExe,
                    Arguments =
                        "--apply-update" +
                        " --update-url " + CommandLineQuote(zipUrl) +
                        " --update-target " + CommandLineQuote(appDir) +
                        " --update-exe " + CommandLineQuote(exePath) +
                        " --update-temp " + CommandLineQuote(updaterTempDir) +
                        " --update-wait-pid " + Process.GetCurrentProcess().Id,
                    WorkingDirectory = updaterRoot,
                    UseShellExecute = false,
                    CreateNoWindow = true
                });
                if (exitApp != null) exitApp();
            }
            catch (Exception ex)
            {
                if (silent) return;
                MessageBox.Show(owner, ex.Message, "Clipman Update", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private static List<GitHubReleaseInfo> FetchReleases()
        {
            using (var client = CreateGitHubClient())
            {
                var json = client.DownloadString(ApiBaseUrl() + "/releases?per_page=100");
                return ParseReleases(json);
            }
        }

        private static WebClient CreateGitHubClient()
        {
            NetworkSecurity.EnableModernTls();
            var client = new WebClient();
            client.Headers.Add("User-Agent", UserAgent);
            return client;
        }

        private static string ApiBaseUrl()
        {
            return ProjectUrl.Replace("https://github.com/", "https://api.github.com/repos/");
        }

        private static GitHubReleaseInfo LatestWindowsRelease(IEnumerable<GitHubReleaseInfo> releases)
        {
            return (releases ?? new List<GitHubReleaseInfo>())
                .Select(r => new { Release = r, Version = ReleaseVersion(r) })
                .Where(i => i.Version != null && FindPortableZipAsset(i.Release) != null)
                .OrderByDescending(i => i.Version)
                .Select(i => i.Release)
                .FirstOrDefault();
        }

        private static Version ReleaseVersion(GitHubReleaseInfo release)
        {
            if (release == null || string.IsNullOrWhiteSpace(release.TagName)) return null;
            Version version;
            return Version.TryParse(release.TagName.Trim().TrimStart('v', 'V'), out version) ? version : null;
        }

        private static string BuildUpdateReleaseNotes(IEnumerable<GitHubReleaseInfo> releases, Version current, Version latest)
        {
            var newerReleases = (releases ?? new List<GitHubReleaseInfo>())
                .Select(r => new { Release = r, Version = ReleaseVersion(r) })
                .Where(i => i.Version != null && i.Version > current && i.Version <= latest)
                .OrderBy(i => i.Version)
                .ToList();

            var builder = new System.Text.StringBuilder();
            builder.AppendLine("Your version: " + current);
            builder.AppendLine("New version: " + latest);
            builder.AppendLine();
            builder.AppendLine("Changes between " + current + " and " + latest);

            if (newerReleases.Count == 0)
            {
                builder.AppendLine();
                builder.AppendLine("No release notes were provided for this update.");
                return builder.ToString().TrimEnd();
            }

            foreach (var item in newerReleases)
            {
                builder.AppendLine();
                builder.AppendLine(item.Release.TagName);
                builder.AppendLine(FormatReleaseNotesForDialog(RemoveDuplicateReleaseHeading(item.Release.Body, item.Release.TagName), "No release notes were provided for this update."));
            }

            return builder.ToString().TrimEnd();
        }

        private static GitHubReleaseAsset FindPortableZipAsset(GitHubReleaseInfo release)
        {
            if (release == null || release.Assets == null) return null;
            var version = ReleaseVersion(release);
            if (version == null) return null;
            var expectedName = "Clipman-" + version + ".zip";
            return release.Assets
                .Where(a => a != null && !string.IsNullOrWhiteSpace(a.BrowserDownloadUrl) && !string.IsNullOrWhiteSpace(a.Name))
                .Where(a => string.Equals(a.Name, expectedName, StringComparison.OrdinalIgnoreCase))
                .FirstOrDefault();
        }

        private static string FormatReleaseNotesForDialog(string markdown, string emptyText)
        {
            if (string.IsNullOrWhiteSpace(markdown)) return emptyText;
            var text = markdown
                .Replace("\r\n", "\n")
                .Replace("\r", "\n");
            var lines = text.Split('\n')
                .Select(line => line.TrimEnd())
                .Select(line => line.StartsWith("#", StringComparison.Ordinal) ? line.TrimStart('#').Trim() : line)
                .Select(line => line.StartsWith("- ", StringComparison.Ordinal) ? "  " + line.Substring(2) : line)
                .Select(line => line.StartsWith("* ", StringComparison.Ordinal) ? "  " + line.Substring(2) : line);
            return string.Join(Environment.NewLine, lines).Trim();
        }

        private static string RemoveDuplicateReleaseHeading(string markdown, string tagName)
        {
            if (string.IsNullOrWhiteSpace(markdown) || string.IsNullOrWhiteSpace(tagName)) return markdown;
            var lines = markdown.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n').ToList();
            var firstContentIndex = lines.FindIndex(line => !string.IsNullOrWhiteSpace(line));
            if (firstContentIndex < 0) return markdown;
            var firstLine = lines[firstContentIndex].Trim();
            if (!firstLine.StartsWith("#", StringComparison.Ordinal)) return markdown;
            var heading = NormalizeReleaseHeading(firstLine.TrimStart('#').Trim());
            var tag = NormalizeReleaseHeading(tagName);
            if (heading.Length > 0 && tag.Length > 0 && (heading == tag || heading.Contains(tag) || tag.Contains(heading)))
            {
                lines.RemoveAt(firstContentIndex);
                return string.Join("\n", lines).TrimStart('\n');
            }

            return markdown;
        }

        private static string NormalizeReleaseHeading(string value)
        {
            if (string.IsNullOrWhiteSpace(value)) return string.Empty;
            var text = value.Trim().ToLowerInvariant();
            text = text.Replace("what's new in", string.Empty).Replace("whats new in", string.Empty).Replace("what is new in", string.Empty).Replace("version", string.Empty).Trim();
            return text.StartsWith("v", StringComparison.Ordinal) ? text.Substring(1) : text;
        }

        private static List<GitHubReleaseInfo> ParseReleases(string json)
        {
            var serializer = new JavaScriptSerializer();
            var rows = serializer.DeserializeObject(json) as object[];
            if (rows == null) return new List<GitHubReleaseInfo>();
            return rows.Select(ParseReleaseObject).Where(r => r != null).ToList();
        }

        private static GitHubReleaseInfo ParseRelease(string json)
        {
            var serializer = new JavaScriptSerializer();
            return ParseReleaseObject(serializer.DeserializeObject(json));
        }

        private static GitHubReleaseInfo ParseReleaseObject(object value)
        {
            var map = value as Dictionary<string, object>;
            if (map == null) return null;
            var release = new GitHubReleaseInfo
            {
                TagName = GetString(map, "tag_name"),
                HtmlUrl = GetString(map, "html_url"),
                Body = GetString(map, "body"),
                Assets = new List<GitHubReleaseAsset>()
            };

            object assetsValue;
            if (map.TryGetValue("assets", out assetsValue))
            {
                var assets = assetsValue as object[];
                if (assets != null)
                {
                    foreach (var assetValue in assets)
                    {
                        var assetMap = assetValue as Dictionary<string, object>;
                        if (assetMap == null) continue;
                        release.Assets.Add(new GitHubReleaseAsset
                        {
                            Name = GetString(assetMap, "name"),
                            BrowserDownloadUrl = GetString(assetMap, "browser_download_url")
                        });
                    }
                }
            }

            return release;
        }

        private static string GetString(Dictionary<string, object> map, string key)
        {
            object value;
            return map.TryGetValue(key, out value) && value != null ? Convert.ToString(value) : string.Empty;
        }

        private static void OpenUrl(string url)
        {
            try
            {
                Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
            }
            catch
            {
            }
        }

        private static string GetUpdaterTempDirectory(string appDir)
        {
            var candidates = new List<string>();
            var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            if (!string.IsNullOrWhiteSpace(localAppData)) candidates.Add(Path.Combine(localAppData, "Temp"));
            candidates.Add(Path.GetTempPath());
            candidates.Add(Path.Combine(appDir, "Settings", "Update Temp"));

            foreach (var candidate in candidates)
            {
                if (string.IsNullOrWhiteSpace(candidate)) continue;
                try
                {
                    var fullPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(candidate));
                    Directory.CreateDirectory(fullPath);
                    return fullPath;
                }
                catch
                {
                }
            }

            throw new InvalidOperationException("Could not create a temporary folder for the updater.");
        }

        private static string CommandLineQuote(string value)
        {
            if (value == null) return "\"\"";
            return "\"" + value.Replace("\"", "\\\"") + "\"";
        }

        private sealed class GitHubReleaseInfo
        {
            public string TagName { get; set; }
            public string HtmlUrl { get; set; }
            public string Body { get; set; }
            public List<GitHubReleaseAsset> Assets { get; set; }
        }

        private sealed class GitHubReleaseAsset
        {
            public string Name { get; set; }
            public string BrowserDownloadUrl { get; set; }
        }
    }
}