mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-20 11:44:59 +00:00
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 <longshuang@msn.cn>
This commit is contained in:
parent
09c0edef8e
commit
7d20f97f4e
5 changed files with 133 additions and 153 deletions
166
src/App.axaml.cs
166
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;
|
||||
}
|
||||
}
|
||||
|
|
97
src/Models/IpcChannel.cs
Normal file
97
src/Models/IpcChannel.cs
Normal file
|
@ -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<string> 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;
|
||||
}
|
||||
}
|
|
@ -752,5 +752,4 @@
|
|||
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
|
||||
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
|
||||
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
|
||||
<x:String x:Key="Text.Preferences.General.OpenReposInNewTab" xml:space="preserve">Open repositories in new tab instead of new window</x:String>
|
||||
</ResourceDictionary>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<TabItem.Header>
|
||||
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.General}"/>
|
||||
</TabItem.Header>
|
||||
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
|
||||
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="{DynamicResource Text.Preferences.General.Locale}"
|
||||
HorizontalAlignment="Right"
|
||||
|
@ -142,11 +142,6 @@
|
|||
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowChildren, Mode=TwoWay}"/>
|
||||
|
||||
<CheckBox Grid.Row="8" Grid.Column="1"
|
||||
Height="32"
|
||||
Content="{DynamicResource Text.Preferences.General.OpenReposInNewTab}"
|
||||
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=OpenReposInNewTab, Mode=TwoWay}"/>
|
||||
|
||||
<CheckBox Grid.Row="9" Grid.Column="1"
|
||||
Height="32"
|
||||
Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}"
|
||||
IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue