Skip to content

Commit 783cef6

Browse files
committed
Gracefully shutdown all threads when exiting
1 parent 1b066a5 commit 783cef6

File tree

4 files changed

+187
-34
lines changed

4 files changed

+187
-34
lines changed

Flow.Launcher/App.xaml.cs

Lines changed: 111 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,29 @@
2525

2626
namespace Flow.Launcher
2727
{
28-
public partial class App : IDisposable, ISingleInstanceApp
28+
public partial class App : IAsyncDisposable, ISingleInstanceApp
2929
{
30+
#region Public Properties
31+
3032
public static IPublicAPI API { get; private set; }
33+
public static CancellationTokenSource NativeThreadCTS { get; private set; }
34+
35+
#endregion
36+
37+
#region Private Fields
38+
3139
private static bool _disposed;
40+
private MainWindow _mainWindow;
41+
private readonly MainViewModel _mainVM;
3242
private readonly Settings _settings;
3343

44+
// To prevent two disposals running at the same time.
45+
private static readonly object _disposingLock = new();
46+
47+
#endregion
48+
49+
#region Constructor
50+
3451
public App()
3552
{
3653
// Initialize settings
@@ -78,34 +95,47 @@ public App()
7895
{
7996
API = Ioc.Default.GetRequiredService<IPublicAPI>();
8097
_settings.Initialize();
98+
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
8199
}
82100
catch (Exception e)
83101
{
84102
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
85103
return;
86104
}
87-
}
88105

89-
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
90-
{
91-
// Firstly show users the message
92-
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
106+
// Local function
107+
static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
108+
{
109+
// Firstly show users the message
110+
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
93111

94-
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
95-
Environment.FailFast(message, e);
112+
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
113+
Environment.FailFast(message, e);
114+
}
96115
}
97116

117+
#endregion
118+
119+
#region Main
120+
98121
[STAThread]
99122
public static void Main()
100123
{
124+
NativeThreadCTS = new CancellationTokenSource();
125+
101126
if (SingleInstance<App>.InitializeAsFirstInstance())
102127
{
103-
using var application = new App();
128+
var application = new App();
104129
application.InitializeComponent();
105130
application.Run();
131+
application.DisposeAsync().AsTask().GetAwaiter().GetResult();
106132
}
107133
}
108134

135+
#endregion
136+
137+
#region App Events
138+
109139
#pragma warning disable VSTHRD100 // Avoid async void methods
110140

111141
private async void OnStartup(object sender, StartupEventArgs e)
@@ -136,11 +166,11 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
136166
await PluginManager.InitializePluginsAsync();
137167
await imageLoadertask;
138168

139-
var window = new MainWindow();
169+
_mainWindow = new MainWindow();
140170

141171
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
142172

143-
Current.MainWindow = window;
173+
Current.MainWindow = _mainWindow;
144174
Current.MainWindow.Title = Constant.FlowLauncher;
145175

146176
HotKeyMapper.Initialize();
@@ -157,8 +187,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
157187
AutoUpdates();
158188

159189
API.SaveAppAllSettings();
160-
Log.Info(
161-
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
190+
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
162191
});
163192
}
164193

@@ -191,7 +220,6 @@ private void AutoStartup()
191220
}
192221
}
193222

194-
//[Conditional("RELEASE")]
195223
private void AutoUpdates()
196224
{
197225
_ = Task.Run(async () =>
@@ -209,11 +237,30 @@ private void AutoUpdates()
209237
});
210238
}
211239

240+
#endregion
241+
242+
#region Register Events
243+
212244
private void RegisterExitEvents()
213245
{
214-
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
215-
Current.Exit += (s, e) => Dispose();
216-
Current.SessionEnding += (s, e) => Dispose();
246+
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
247+
{
248+
Log.Info("|App.RegisterExitEvents|Process Exit");
249+
_ = DisposeAsync();
250+
};
251+
252+
Current.Exit += (s, e) =>
253+
{
254+
NativeThreadCTS.Cancel();
255+
Log.Info("|App.RegisterExitEvents|Application Exit");
256+
_ = DisposeAsync();
257+
};
258+
259+
Current.SessionEnding += (s, e) =>
260+
{
261+
Log.Info("|App.RegisterExitEvents|Session Ending");
262+
_ = DisposeAsync();
263+
};
217264
}
218265

219266
/// <summary>
@@ -234,20 +281,62 @@ private static void RegisterAppDomainExceptions()
234281
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
235282
}
236283

237-
public void Dispose()
284+
#endregion
285+
286+
#region IAsyncDisposable
287+
288+
protected virtual async ValueTask DisposeAsync(bool disposing)
238289
{
239-
// if sessionending is called, exit proverbially be called when log off / shutdown
240-
// but if sessionending is not called, exit won't be called when log off / shutdown
241-
if (!_disposed)
290+
// Prevent two disposes at the same time.
291+
lock (_disposingLock)
242292
{
243-
API.SaveAppAllSettings();
293+
if (!disposing)
294+
{
295+
return;
296+
}
297+
298+
if (_disposed)
299+
{
300+
return;
301+
}
302+
244303
_disposed = true;
245304
}
305+
306+
await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () =>
307+
{
308+
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
309+
310+
if (disposing)
311+
{
312+
API?.SaveAppAllSettings();
313+
await PluginManager.DisposePluginsAsync();
314+
315+
// Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed.
316+
await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync);
317+
_mainVM?.Dispose();
318+
}
319+
320+
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
321+
});
246322
}
247323

324+
public async ValueTask DisposeAsync()
325+
{
326+
// Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method
327+
await DisposeAsync(disposing: true);
328+
GC.SuppressFinalize(this);
329+
}
330+
331+
#endregion
332+
333+
#region ISingleInstanceApp
334+
248335
public void OnSecondAppStarted()
249336
{
250337
Ioc.Default.GetRequiredService<MainViewModel>().Show();
251338
}
339+
340+
#endregion
252341
}
253342
}

Flow.Launcher/MainWindow.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AllowDrop="True"
1818
AllowsTransparency="True"
1919
Background="Transparent"
20+
Closed="OnClosed"
2021
Closing="OnClosing"
2122
Deactivated="OnDeactivated"
2223
Icon="Images/app.png"

Flow.Launcher/MainWindow.xaml.cs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
namespace Flow.Launcher
2929
{
30-
public partial class MainWindow
30+
public partial class MainWindow : IDisposable
3131
{
3232
#region Private Fields
3333

@@ -50,13 +50,17 @@ public partial class MainWindow
5050
private SoundPlayer animationSoundWPF;
5151

5252
// Window WndProc
53+
private HwndSource _hwndSource;
5354
private int _initialWidth;
5455
private int _initialHeight;
5556

5657
// Window Animation
5758
private const double DefaultRightMargin = 66; //* this value from base.xaml
5859
private bool _animating;
59-
private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부
60+
private bool _isClockPanelAnimating = false;
61+
62+
// IDisposable
63+
private bool _disposedValue = false;
6064

6165
#endregion
6266

@@ -85,8 +89,8 @@ public MainWindow()
8589
private void OnSourceInitialized(object sender, EventArgs e)
8690
{
8791
var handle = Win32Helper.GetWindowHandle(this, true);
88-
var win = HwndSource.FromHwnd(handle);
89-
win.AddHook(WndProc);
92+
_hwndSource = HwndSource.FromHwnd(handle);
93+
_hwndSource.AddHook(WndProc);
9094
Win32Helper.HideFromAltTab(this);
9195
Win32Helper.DisableControlBox(this);
9296
}
@@ -227,20 +231,31 @@ private async void OnLoaded(object sender, RoutedEventArgs _)
227231
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
228232
}
229233

230-
private async void OnClosing(object sender, CancelEventArgs e)
234+
private void OnClosing(object sender, CancelEventArgs e)
231235
{
236+
_viewModel.Save();
232237
_notifyIcon.Visible = false;
233-
App.API.SaveAppAllSettings();
234-
e.Cancel = true;
235-
await PluginManager.DisposePluginsAsync();
236238
Notification.Uninstall();
237-
Environment.Exit(0);
239+
}
240+
241+
private void OnClosed(object sender, EventArgs e)
242+
{
243+
try
244+
{
245+
_hwndSource.RemoveHook(WndProc);
246+
}
247+
catch (Exception)
248+
{
249+
// Ignored
250+
}
251+
252+
_hwndSource = null;
238253
}
239254

240255
private void OnLocationChanged(object sender, EventArgs e)
241256
{
242-
if (_animating)
243-
return;
257+
if (_animating) return;
258+
244259
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
245260
{
246261
_settings.WindowLeft = Left;
@@ -990,5 +1005,29 @@ private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e)
9901005
}
9911006

9921007
#endregion
1008+
1009+
#region IDisposable
1010+
1011+
protected virtual void Dispose(bool disposing)
1012+
{
1013+
if (!_disposedValue)
1014+
{
1015+
if (disposing)
1016+
{
1017+
_hwndSource?.Dispose();
1018+
}
1019+
1020+
_disposedValue = true;
1021+
}
1022+
}
1023+
1024+
public void Dispose()
1025+
{
1026+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
1027+
Dispose(disposing: true);
1028+
GC.SuppressFinalize(this);
1029+
}
1030+
1031+
#endregion
9931032
}
9941033
}

Flow.Launcher/ViewModel/MainViewModel.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
namespace Flow.Launcher.ViewModel
2929
{
30-
public partial class MainViewModel : BaseModel, ISavable
30+
public partial class MainViewModel : BaseModel, ISavable, IDisposable
3131
{
3232
#region Private Fields
3333

@@ -1542,5 +1542,29 @@ public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
15421542
}
15431543

15441544
#endregion
1545+
1546+
#region IDisposable
1547+
1548+
private bool _disposed = false;
1549+
1550+
protected virtual void Dispose(bool disposing)
1551+
{
1552+
if (!_disposed)
1553+
{
1554+
if (disposing)
1555+
{
1556+
_updateSource?.Dispose();
1557+
_disposed = true;
1558+
}
1559+
}
1560+
}
1561+
1562+
public void Dispose()
1563+
{
1564+
Dispose(disposing: true);
1565+
GC.SuppressFinalize(this);
1566+
}
1567+
1568+
#endregion
15451569
}
15461570
}

0 commit comments

Comments
 (0)