From 7d20f97f4e48988c639efe80be34a8018dd2d9b3 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 14 Apr 2025 22:03:51 +0800 Subject: [PATCH] code_review: PR #1185 - make `SourceGit` running in singleton mode - `TrySendArgsToExistingInstance` should not be called before `BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)` since we may want to launch `SourceGit` as a core editor. - avoid `preference.json` to be saved by multiple instances. - move IPC code to models. Signed-off-by: leo --- src/App.axaml.cs | 166 +++++------------------------- src/Models/IpcChannel.cs | 97 +++++++++++++++++ src/Resources/Locales/en_US.axaml | 1 - src/ViewModels/Preferences.cs | 15 ++- src/Views/Preferences.axaml | 7 +- 5 files changed, 133 insertions(+), 153 deletions(-) create mode 100644 src/Models/IpcChannel.cs diff --git a/src/App.axaml.cs b/src/App.axaml.cs index b1be7072..6504e85b 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.IO.Pipes; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Linq; using Avalonia; using Avalonia.Controls; @@ -48,8 +46,6 @@ namespace SourceGit Environment.Exit(exitTodo); else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage)) Environment.Exit(exitMessage); - else if (TrySendArgsToExistingInstance(args)) - Environment.Exit(0); else BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } @@ -81,44 +77,6 @@ namespace SourceGit return builder; } - private static bool TrySendArgsToExistingInstance(string[] args) - { - if (args == null || args.Length != 1 || !Directory.Exists(args[0])) - return false; - - var pref = ViewModels.Preferences.Instance; - - if (!pref.OpenReposInNewTab) - return false; - - try - { - var processes = Process.GetProcessesByName("SourceGit"); - - if (processes.Length <= 1) - return false; - - using var client = new NamedPipeClientStream(".", "SourceGitIPC", PipeDirection.Out); - - client.Connect(1000); - - if (client.IsConnected) - { - using var writer = new StreamWriter(client); - - writer.WriteLine(args[0]); - writer.Flush(); - - return true; - } - } - catch (Exception) - { - } - - return false; - } - private static void LogException(Exception ex) { if (ex == null) @@ -370,13 +328,7 @@ namespace SourceGit AvaloniaXamlLoader.Load(this); var pref = ViewModels.Preferences.Instance; - - pref.PropertyChanged += (s, e) => { - pref.Save(); - - if (e.PropertyName.Equals(nameof(ViewModels.Preferences.OpenReposInNewTab))) - HandleOpenReposInNewTabChanged(); - }; + pref.PropertyChanged += (_, _) => pref.Save(); SetLocale(pref.Locale); SetTheme(pref.Theme, pref.ThemeOverrides); @@ -395,7 +347,17 @@ namespace SourceGit if (TryLaunchAsAskpass(desktop)) return; - TryLaunchAsNormal(desktop); + _ipcChannel = new Models.IpcChannel(); + if (!_ipcChannel.IsFirstInstance) + { + _ipcChannel.SendToFirstInstance(desktop.Args is { Length: 1 } ? desktop.Args[0] : string.Empty); + Quit(0); + } + else + { + _ipcChannel.MessageReceived += TryOpenRepository; + TryLaunchAsNormal(desktop); + } } } #endregion @@ -533,12 +495,12 @@ namespace SourceGit if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) startupRepo = desktop.Args[0]; + var pref = ViewModels.Preferences.Instance; + pref.SetCanModify(); + _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; - - var pref = ViewModels.Preferences.Instance; - - HandleOpenReposInNewTabChanged(); + desktop.Exit += (_, _) => _ipcChannel.Dispose(); #if !DISABLE_UPDATE_DETECTION if (pref.ShouldCheck4UpdateOnStartup()) @@ -546,91 +508,20 @@ namespace SourceGit #endif } - private void HandleOpenReposInNewTabChanged() + private void TryOpenRepository(string repo) { - var pref = ViewModels.Preferences.Instance; - - if (pref.OpenReposInNewTab) + if (string.IsNullOrEmpty(repo) || !Directory.Exists(repo)) + return; + + var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd(); + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { - if (_ipcServerTask == null || _ipcServerTask.IsCompleted) + Dispatcher.UIThread.Invoke(() => { - // Start IPC server - _ipcServerCts = new CancellationTokenSource(); - _ipcServerTask = Task.Run(() => StartIPCServer(_ipcServerCts.Token)); - } - } - else - { - // Stop IPC server if running - if (_ipcServerCts != null && !_ipcServerCts.IsCancellationRequested) - { - _ipcServerCts.Cancel(); - _ipcServerCts.Dispose(); - _ipcServerCts = null; - } - _ipcServerTask = null; - } - } - - private void StartIPCServer(CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { - using var server = new NamedPipeServerStream("SourceGitIPC", PipeDirection.In); - - // Use WaitForConnectionAsync with cancellation token - try - { - Task connectionTask = server.WaitForConnectionAsync(cancellationToken); - connectionTask.Wait(cancellationToken); - } - catch (OperationCanceledException) - { - return; - } - catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException)) - { - return; - } - - // Process the connection - using var reader = new StreamReader(server); - var repoPath = reader.ReadLine(); - - if (!string.IsNullOrEmpty(repoPath) && Directory.Exists(repoPath)) - { - Dispatcher.UIThread.Post(() => - { - try - { - var test = new Commands.QueryRepositoryRootPath(repoPath).ReadToEnd(); - - if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) - { - var repoRootPath = test.StdOut.Trim(); - var pref = ViewModels.Preferences.Instance; - var node = pref.FindOrAddNodeByRepositoryPath(repoRootPath, null, false); - - ViewModels.Welcome.Instance.Refresh(); - - _launcher?.OpenRepositoryInTab(node, null); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - desktop.MainWindow.Activate(); - } - } - catch (Exception) - { - } - }); - } - } - } - catch (Exception) - { - // Pipe server failed, we can just exit the thread + var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false); + ViewModels.Welcome.Instance.Refresh(); + _launcher?.OpenRepositoryInTab(node, null); + }); } } @@ -719,11 +610,10 @@ namespace SourceGit return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } + private Models.IpcChannel _ipcChannel = null; private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; private ResourceDictionary _fontsOverrides = null; - private Task _ipcServerTask = null; - private CancellationTokenSource _ipcServerCts = null; } } diff --git a/src/Models/IpcChannel.cs b/src/Models/IpcChannel.cs new file mode 100644 index 00000000..e2a34b29 --- /dev/null +++ b/src/Models/IpcChannel.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SourceGit.Models +{ + public class IpcChannel : IDisposable + { + public bool IsFirstInstance + { + get => _server != null; + } + + public event Action MessageReceived; + + public IpcChannel() + { + try + { + _server = new NamedPipeServerStream("SourceGitIPCChannel", PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + _cancellationTokenSource = new CancellationTokenSource(); + Task.Run(StartServer); + } + catch + { + // IGNORE + } + } + + public void SendToFirstInstance(string cmd) + { + try + { + using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel", PipeDirection.Out)) + { + client.Connect(1000); + if (client.IsConnected) + { + using (var writer = new StreamWriter(client)) + { + writer.Write(Encoding.UTF8.GetBytes(cmd)); + writer.Flush(); + } + } + } + } + catch + { + // IGNORE + } + } + + public void Dispose() + { + _server?.Close(); + } + + private async void StartServer() + { + var buffer = new byte[1024]; + + while (true) + { + try + { + await _server.WaitForConnectionAsync(_cancellationTokenSource.Token); + + using (var stream = new MemoryStream()) + { + while (true) + { + var readed = await _server.ReadAsync(buffer.AsMemory(0, 1024), _cancellationTokenSource.Token); + if (readed == 0) + break; + + stream.Write(buffer, 0, readed); + } + + stream.Seek(0, SeekOrigin.Begin); + MessageReceived?.Invoke(Encoding.UTF8.GetString(stream.ToArray()).Trim()); + _server.Disconnect(); + } + } + catch + { + // IGNORE + } + } + } + + private NamedPipeServerStream _server = null; + private CancellationTokenSource _cancellationTokenSource = null; + } +} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 123b6381..7e931998 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -752,5 +752,4 @@ Lock Remove Unlock - Open repositories in new tab instead of new window diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index d1e78d6a..9395e0a7 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -90,12 +90,6 @@ namespace SourceGit.ViewModels } } - public bool OpenReposInNewTab - { - get => _openReposInNewTab; - set => SetProperty(ref _openReposInNewTab, value); - } - public bool UseSystemWindowFrame { get => _useSystemWindowFrame; @@ -369,6 +363,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _lastCheckUpdateTime, value); } + public void SetCanModify() + { + _isReadonly = false; + } + public bool IsGitConfigured() { var path = GitInstallPath; @@ -496,7 +495,7 @@ namespace SourceGit.ViewModels public void Save() { - if (_isLoading) + if (_isLoading || _isReadonly) return; var file = Path.Combine(Native.OS.DataDir, "preference.json"); @@ -632,13 +631,13 @@ namespace SourceGit.ViewModels private static Preferences _instance = null; private static bool _isLoading = false; + private bool _isReadonly = true; private string _locale = "en_US"; private string _theme = "Default"; private string _themeOverrides = string.Empty; private string _defaultFontFamily = string.Empty; private string _monospaceFontFamily = string.Empty; private bool _onlyUseMonoFontInEditor = false; - private bool _openReposInNewTab = false; private bool _useSystemWindowFrame = false; private double _defaultFontSize = 13; private double _editorFontSize = 13; diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 704757df..702ec20f 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -46,7 +46,7 @@ - + - -