Skip to content

Commit 7d20f97

Browse files
committed
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 <[email protected]>
1 parent 09c0ede commit 7d20f97

File tree

5 files changed

+132
-152
lines changed

5 files changed

+132
-152
lines changed

src/App.axaml.cs

Lines changed: 27 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.IO;
5-
using System.IO.Pipes;
65
using System.Net.Http;
76
using System.Reflection;
87
using System.Text;
98
using System.Text.Json;
109
using System.Threading;
1110
using System.Threading.Tasks;
12-
using System.Linq;
1311

1412
using Avalonia;
1513
using Avalonia.Controls;
@@ -48,8 +46,6 @@ public static void Main(string[] args)
4846
Environment.Exit(exitTodo);
4947
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
5048
Environment.Exit(exitMessage);
51-
else if (TrySendArgsToExistingInstance(args))
52-
Environment.Exit(0);
5349
else
5450
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
5551
}
@@ -81,44 +77,6 @@ public static AppBuilder BuildAvaloniaApp()
8177
return builder;
8278
}
8379

84-
private static bool TrySendArgsToExistingInstance(string[] args)
85-
{
86-
if (args == null || args.Length != 1 || !Directory.Exists(args[0]))
87-
return false;
88-
89-
var pref = ViewModels.Preferences.Instance;
90-
91-
if (!pref.OpenReposInNewTab)
92-
return false;
93-
94-
try
95-
{
96-
var processes = Process.GetProcessesByName("SourceGit");
97-
98-
if (processes.Length <= 1)
99-
return false;
100-
101-
using var client = new NamedPipeClientStream(".", "SourceGitIPC", PipeDirection.Out);
102-
103-
client.Connect(1000);
104-
105-
if (client.IsConnected)
106-
{
107-
using var writer = new StreamWriter(client);
108-
109-
writer.WriteLine(args[0]);
110-
writer.Flush();
111-
112-
return true;
113-
}
114-
}
115-
catch (Exception)
116-
{
117-
}
118-
119-
return false;
120-
}
121-
12280
private static void LogException(Exception ex)
12381
{
12482
if (ex == null)
@@ -370,13 +328,7 @@ public override void Initialize()
370328
AvaloniaXamlLoader.Load(this);
371329

372330
var pref = ViewModels.Preferences.Instance;
373-
374-
pref.PropertyChanged += (s, e) => {
375-
pref.Save();
376-
377-
if (e.PropertyName.Equals(nameof(ViewModels.Preferences.OpenReposInNewTab)))
378-
HandleOpenReposInNewTabChanged();
379-
};
331+
pref.PropertyChanged += (_, _) => pref.Save();
380332

381333
SetLocale(pref.Locale);
382334
SetTheme(pref.Theme, pref.ThemeOverrides);
@@ -395,7 +347,17 @@ public override void OnFrameworkInitializationCompleted()
395347
if (TryLaunchAsAskpass(desktop))
396348
return;
397349

398-
TryLaunchAsNormal(desktop);
350+
_ipcChannel = new Models.IpcChannel();
351+
if (!_ipcChannel.IsFirstInstance)
352+
{
353+
_ipcChannel.SendToFirstInstance(desktop.Args is { Length: 1 } ? desktop.Args[0] : string.Empty);
354+
Quit(0);
355+
}
356+
else
357+
{
358+
_ipcChannel.MessageReceived += TryOpenRepository;
359+
TryLaunchAsNormal(desktop);
360+
}
399361
}
400362
}
401363
#endregion
@@ -533,104 +495,33 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
533495
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
534496
startupRepo = desktop.Args[0];
535497

536-
_launcher = new ViewModels.Launcher(startupRepo);
537-
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
538-
539498
var pref = ViewModels.Preferences.Instance;
499+
pref.SetCanModify();
540500

541-
HandleOpenReposInNewTabChanged();
501+
_launcher = new ViewModels.Launcher(startupRepo);
502+
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
503+
desktop.Exit += (_, _) => _ipcChannel.Dispose();
542504

543505
#if !DISABLE_UPDATE_DETECTION
544506
if (pref.ShouldCheck4UpdateOnStartup())
545507
Check4Update();
546508
#endif
547509
}
548510

549-
private void HandleOpenReposInNewTabChanged()
511+
private void TryOpenRepository(string repo)
550512
{
551-
var pref = ViewModels.Preferences.Instance;
552-
553-
if (pref.OpenReposInNewTab)
554-
{
555-
if (_ipcServerTask == null || _ipcServerTask.IsCompleted)
556-
{
557-
// Start IPC server
558-
_ipcServerCts = new CancellationTokenSource();
559-
_ipcServerTask = Task.Run(() => StartIPCServer(_ipcServerCts.Token));
560-
}
561-
}
562-
else
563-
{
564-
// Stop IPC server if running
565-
if (_ipcServerCts != null && !_ipcServerCts.IsCancellationRequested)
566-
{
567-
_ipcServerCts.Cancel();
568-
_ipcServerCts.Dispose();
569-
_ipcServerCts = null;
570-
}
571-
_ipcServerTask = null;
572-
}
573-
}
513+
if (string.IsNullOrEmpty(repo) || !Directory.Exists(repo))
514+
return;
574515

575-
private void StartIPCServer(CancellationToken cancellationToken)
576-
{
577-
try
516+
var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd();
517+
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
578518
{
579-
while (!cancellationToken.IsCancellationRequested)
519+
Dispatcher.UIThread.Invoke(() =>
580520
{
581-
using var server = new NamedPipeServerStream("SourceGitIPC", PipeDirection.In);
582-
583-
// Use WaitForConnectionAsync with cancellation token
584-
try
585-
{
586-
Task connectionTask = server.WaitForConnectionAsync(cancellationToken);
587-
connectionTask.Wait(cancellationToken);
588-
}
589-
catch (OperationCanceledException)
590-
{
591-
return;
592-
}
593-
catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException))
594-
{
595-
return;
596-
}
597-
598-
// Process the connection
599-
using var reader = new StreamReader(server);
600-
var repoPath = reader.ReadLine();
601-
602-
if (!string.IsNullOrEmpty(repoPath) && Directory.Exists(repoPath))
603-
{
604-
Dispatcher.UIThread.Post(() =>
605-
{
606-
try
607-
{
608-
var test = new Commands.QueryRepositoryRootPath(repoPath).ReadToEnd();
609-
610-
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
611-
{
612-
var repoRootPath = test.StdOut.Trim();
613-
var pref = ViewModels.Preferences.Instance;
614-
var node = pref.FindOrAddNodeByRepositoryPath(repoRootPath, null, false);
615-
616-
ViewModels.Welcome.Instance.Refresh();
617-
618-
_launcher?.OpenRepositoryInTab(node, null);
619-
620-
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null)
621-
desktop.MainWindow.Activate();
622-
}
623-
}
624-
catch (Exception)
625-
{
626-
}
627-
});
628-
}
629-
}
630-
}
631-
catch (Exception)
632-
{
633-
// Pipe server failed, we can just exit the thread
521+
var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false);
522+
ViewModels.Welcome.Instance.Refresh();
523+
_launcher?.OpenRepositoryInTab(node, null);
524+
});
634525
}
635526
}
636527

@@ -719,11 +610,10 @@ private string FixFontFamilyName(string input)
719610
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
720611
}
721612

613+
private Models.IpcChannel _ipcChannel = null;
722614
private ViewModels.Launcher _launcher = null;
723615
private ResourceDictionary _activeLocale = null;
724616
private ResourceDictionary _themeOverrides = null;
725617
private ResourceDictionary _fontsOverrides = null;
726-
private Task _ipcServerTask = null;
727-
private CancellationTokenSource _ipcServerCts = null;
728618
}
729619
}

src/Models/IpcChannel.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Pipes;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace SourceGit.Models
9+
{
10+
public class IpcChannel : IDisposable
11+
{
12+
public bool IsFirstInstance
13+
{
14+
get => _server != null;
15+
}
16+
17+
public event Action<string> MessageReceived;
18+
19+
public IpcChannel()
20+
{
21+
try
22+
{
23+
_server = new NamedPipeServerStream("SourceGitIPCChannel", PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
24+
_cancellationTokenSource = new CancellationTokenSource();
25+
Task.Run(StartServer);
26+
}
27+
catch
28+
{
29+
// IGNORE
30+
}
31+
}
32+
33+
public void SendToFirstInstance(string cmd)
34+
{
35+
try
36+
{
37+
using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel", PipeDirection.Out))
38+
{
39+
client.Connect(1000);
40+
if (client.IsConnected)
41+
{
42+
using (var writer = new StreamWriter(client))
43+
{
44+
writer.Write(Encoding.UTF8.GetBytes(cmd));
45+
writer.Flush();
46+
}
47+
}
48+
}
49+
}
50+
catch
51+
{
52+
// IGNORE
53+
}
54+
}
55+
56+
public void Dispose()
57+
{
58+
_server?.Close();
59+
}
60+
61+
private async void StartServer()
62+
{
63+
var buffer = new byte[1024];
64+
65+
while (true)
66+
{
67+
try
68+
{
69+
await _server.WaitForConnectionAsync(_cancellationTokenSource.Token);
70+
71+
using (var stream = new MemoryStream())
72+
{
73+
while (true)
74+
{
75+
var readed = await _server.ReadAsync(buffer.AsMemory(0, 1024), _cancellationTokenSource.Token);
76+
if (readed == 0)
77+
break;
78+
79+
stream.Write(buffer, 0, readed);
80+
}
81+
82+
stream.Seek(0, SeekOrigin.Begin);
83+
MessageReceived?.Invoke(Encoding.UTF8.GetString(stream.ToArray()).Trim());
84+
_server.Disconnect();
85+
}
86+
}
87+
catch
88+
{
89+
// IGNORE
90+
}
91+
}
92+
}
93+
94+
private NamedPipeServerStream _server = null;
95+
private CancellationTokenSource _cancellationTokenSource = null;
96+
}
97+
}

src/Resources/Locales/en_US.axaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,5 +752,4 @@
752752
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
753753
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
754754
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
755-
<x:String x:Key="Text.Preferences.General.OpenReposInNewTab" xml:space="preserve">Open repositories in new tab instead of new window</x:String>
756755
</ResourceDictionary>

src/ViewModels/Preferences.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,6 @@ public bool OnlyUseMonoFontInEditor
9090
}
9191
}
9292

93-
public bool OpenReposInNewTab
94-
{
95-
get => _openReposInNewTab;
96-
set => SetProperty(ref _openReposInNewTab, value);
97-
}
98-
9993
public bool UseSystemWindowFrame
10094
{
10195
get => _useSystemWindowFrame;
@@ -369,6 +363,11 @@ public double LastCheckUpdateTime
369363
set => SetProperty(ref _lastCheckUpdateTime, value);
370364
}
371365

366+
public void SetCanModify()
367+
{
368+
_isReadonly = false;
369+
}
370+
372371
public bool IsGitConfigured()
373372
{
374373
var path = GitInstallPath;
@@ -496,7 +495,7 @@ public void AutoRemoveInvalidNode()
496495

497496
public void Save()
498497
{
499-
if (_isLoading)
498+
if (_isLoading || _isReadonly)
500499
return;
501500

502501
var file = Path.Combine(Native.OS.DataDir, "preference.json");
@@ -632,13 +631,13 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
632631
private static Preferences _instance = null;
633632
private static bool _isLoading = false;
634633

634+
private bool _isReadonly = true;
635635
private string _locale = "en_US";
636636
private string _theme = "Default";
637637
private string _themeOverrides = string.Empty;
638638
private string _defaultFontFamily = string.Empty;
639639
private string _monospaceFontFamily = string.Empty;
640640
private bool _onlyUseMonoFontInEditor = false;
641-
private bool _openReposInNewTab = false;
642641
private bool _useSystemWindowFrame = false;
643642
private double _defaultFontSize = 13;
644643
private double _editorFontSize = 13;

0 commit comments

Comments
 (0)