Skip to content

Commit 06bb611

Browse files
committed
長時間処理にスピナーを導入してユーザー向けフィードバックを強化
1 parent f921a27 commit 06bb611

5 files changed

Lines changed: 197 additions & 1 deletion

File tree

Common/Constants.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,26 @@ public static class Constants
758758
/// </summary>
759759
public const string LOG_FAILED_CLEANUP_OLD_LOGS = "Failed to clean up old log files in '{0}'.";
760760

761+
/// <summary>
762+
/// レポート生成スピナーのラベル。
763+
/// </summary>
764+
public const string SPINNER_LABEL_GENERATING_REPORT = "Generating report";
765+
766+
/// <summary>
767+
/// レポート生成完了ログ。
768+
/// </summary>
769+
public const string LOG_REPORT_GENERATION_COMPLETED = "Report generation completed.";
770+
771+
/// <summary>
772+
/// フォルダ比較スピナーのラベル。
773+
/// </summary>
774+
public const string SPINNER_LABEL_FOLDER_DIFF = "Diffing folders";
775+
776+
/// <summary>
777+
/// フォルダ比較完了ログ。
778+
/// </summary>
779+
public const string LOG_FOLDER_DIFF_COMPLETED = "Folder diff completed.";
780+
761781
/// <summary>
762782
/// IL diff 失敗ログ
763783
/// </summary>

Services/FolderDiffService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ public async Task ExecuteFolderDiffAsync()
157157
FileDiffResultLists.RemovedFilesAbsolutePath.Clear();
158158
FileDiffResultLists.ModifiedFilesRelativePath.Clear();
159159
FileDiffResultLists.FileRelativePathToDiffDetailDictionary.Clear();
160+
using var spinner = new ConsoleSpinner(Constants.SPINNER_LABEL_FOLDER_DIFF);
161+
var folderDiffCompleted = false;
160162
try
161163
{
162164
// 長時間の事前処理が続く場合でも利用者に動作中であることを知らせる。
@@ -191,6 +193,7 @@ public async Task ExecuteFolderDiffAsync()
191193
if (totalFilesRelativePathCount == 0)
192194
{
193195
_progressReporter.ReportProgress(100);
196+
folderDiffCompleted = true;
194197
return;
195198
}
196199
}
@@ -287,12 +290,17 @@ public async Task ExecuteFolderDiffAsync()
287290
processedFileCount++;
288291
_progressReporter.ReportProgress((double)processedFileCount * 100.0 / totalFilesRelativePathCount);
289292
}
293+
folderDiffCompleted = true;
290294
}
291295
catch (Exception)
292296
{
293297
LoggerService.LogMessage(LoggerService.LogLevel.Error, string.Format(Constants.ERROR_DIFFING, _oldFolderAbsolutePath, _newFolderAbsolutePath), shouldOutputMessageToConsole: true);
294298
throw;
295299
}
300+
finally
301+
{
302+
spinner.Complete(folderDiffCompleted ? Constants.LOG_FOLDER_DIFF_COMPLETED : null);
303+
}
296304
}
297305

298306
/// <summary>

Services/ReportGenerateService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public void GenerateDiffReport(
3737
{
3838
LoggerService.LogMessage(LoggerService.LogLevel.Warning, Constants.WARNING_MD5_MISMATCH, shouldOutputMessageToConsole: true);
3939
}
40+
using var spinner = new ConsoleSpinner(Constants.SPINNER_LABEL_GENERATING_REPORT);
41+
var reportGenerated = false;
4042
try
4143
{
4244
Utility.ValidateAbsolutePathLengthOrThrow(diffReportAbsolutePath);
@@ -197,6 +199,7 @@ public void GenerateDiffReport(
197199
streamWriter.WriteLine(string.Format(Constants.REPORT_WARNING_LINE, Constants.WARNING_MD5_MISMATCH));
198200
}
199201
}
202+
reportGenerated = true;
200203
}
201204
catch (Exception)
202205
{
@@ -214,6 +217,7 @@ public void GenerateDiffReport(
214217
{
215218
LoggerService.LogMessage(LoggerService.LogLevel.Warning, ex.Message, shouldOutputMessageToConsole: true, ex);
216219
}
220+
spinner.Complete(reportGenerated ? Constants.LOG_REPORT_GENERATION_COMPLETED : null);
217221
}
218222
}
219223
}

Utils/ConsoleSpinner.cs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace FolderDiffIL4DotNet.Utils
6+
{
7+
/// <summary>
8+
/// シンプルなコンソールスピナー。指定したラベルと共に一定間隔で回転する文字を表示します。
9+
/// 無音区間でもユーザーに処理継続中であることを示す用途を想定しています。
10+
/// </summary>
11+
public sealed class ConsoleSpinner : IDisposable
12+
{
13+
#region private constants
14+
/// <summary>
15+
/// 既定のフレーム更新間隔(ミリ秒)。
16+
/// </summary>
17+
private const int DEFAULT_INTERVAL_MILLISECONDS = 120;
18+
#endregion
19+
20+
#region private readonly member variables
21+
/// <summary>
22+
/// デフォルトのスピナーフレーム。
23+
/// </summary>
24+
private static readonly char[] DefaultFrames = ['|', '/', '-', '\\'];
25+
26+
/// <summary>
27+
/// スピナー前に表示するラベル。
28+
/// </summary>
29+
private readonly string _label;
30+
31+
/// <summary>
32+
/// スピナーに使用するフレーム集合。
33+
/// </summary>
34+
private readonly char[] _frames;
35+
36+
/// <summary>
37+
/// フレーム更新間隔。
38+
/// </summary>
39+
private readonly TimeSpan _interval;
40+
41+
/// <summary>
42+
/// アニメーション停止用のキャンセルトークン。
43+
/// </summary>
44+
private readonly CancellationTokenSource _cts = new();
45+
46+
/// <summary>
47+
/// スピナーアニメーションを実行するタスク。
48+
/// </summary>
49+
private readonly Task _animationTask;
50+
#endregion
51+
52+
#region private member variables
53+
/// <summary>
54+
/// 現在表示しているフレームインデックス。
55+
/// </summary>
56+
private int _frameIndex;
57+
58+
/// <summary>
59+
/// 直近に描画したコンテンツの文字数(消去用)。
60+
/// </summary>
61+
private int _lastRenderLength;
62+
63+
/// <summary>
64+
/// 停止済みかどうか。
65+
/// </summary>
66+
private bool _isStopped;
67+
#endregion
68+
69+
/// <summary>
70+
/// スピナーを開始します。
71+
/// </summary>
72+
/// <param name="label">スピナーの前に表示するラベル。</param>
73+
/// <param name="intervalMilliseconds">フレーム更新間隔(ミリ秒)。</param>
74+
/// <param name="frames">スピナーで使用するフレーム文字(省略時は | / - \)。</param>
75+
public ConsoleSpinner(string label, int intervalMilliseconds = DEFAULT_INTERVAL_MILLISECONDS, char[] frames = null)
76+
{
77+
_label = label;
78+
_frames = frames ?? DefaultFrames;
79+
_interval = TimeSpan.FromMilliseconds(intervalMilliseconds);
80+
_animationTask = Task.Run(SpinAsync);
81+
}
82+
83+
/// <summary>
84+
/// 指定間隔でスピナーフレームを更新しつつコンソールへ描画する内部ループ。
85+
/// </summary>
86+
private async Task SpinAsync()
87+
{
88+
while (!_cts.IsCancellationRequested)
89+
{
90+
var prefix = string.IsNullOrEmpty(_label) ? string.Empty : _label + " ";
91+
var frame = _frames[_frameIndex++ % _frames.Length];
92+
var text = $"{prefix}{frame}";
93+
_lastRenderLength = text.Length;
94+
Console.Write($"\r{text}");
95+
Console.Out.Flush();
96+
try
97+
{
98+
await Task.Delay(_interval, _cts.Token);
99+
}
100+
catch (TaskCanceledException)
101+
{
102+
break;
103+
}
104+
}
105+
}
106+
107+
/// <summary>
108+
/// 直近で描画したスピナー行を消去します。
109+
/// </summary>
110+
private void ClearLine()
111+
{
112+
if (_lastRenderLength <= 0)
113+
{
114+
return;
115+
}
116+
Console.Write("\r" + new string(' ', _lastRenderLength) + "\r");
117+
Console.Out.Flush();
118+
}
119+
120+
/// <summary>
121+
/// スピナー描画ループを停止し、残っているアニメーション行をクリアします。
122+
/// </summary>
123+
private void StopInternal()
124+
{
125+
if (_isStopped)
126+
{
127+
return;
128+
}
129+
_isStopped = true;
130+
_cts.Cancel();
131+
try
132+
{
133+
_animationTask.Wait();
134+
}
135+
catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
136+
{
137+
// ignore cancellation
138+
}
139+
ClearLine();
140+
}
141+
142+
/// <summary>
143+
/// スピナーを停止し、任意の完了メッセージを出力します。
144+
/// </summary>
145+
/// <param name="completionMessage">停止後に出力するメッセージ。</param>
146+
public void Complete(string completionMessage = null)
147+
{
148+
StopInternal();
149+
if (!string.IsNullOrEmpty(completionMessage))
150+
{
151+
Console.WriteLine(completionMessage);
152+
}
153+
}
154+
155+
/// <summary>
156+
/// スピナーを停止します。
157+
/// </summary>
158+
public void Dispose()
159+
{
160+
StopInternal();
161+
_cts.Dispose();
162+
}
163+
}
164+
}

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "1.1.4",
3+
"version": "1.1.5",
44
"publicReleaseRefSpec": [
55
"^refs/heads/main$",
66
"^refs/tags/v\\d+\\.\\d+(?:\\.\\d+)?$"

0 commit comments

Comments
 (0)