launcher: friendly errors + Retry / Get help buttons

Raw exception messages ("Access is denied", HttpRequestException
stack traces) gave the user nothing to do. The flow now translates
the common failures into plain language and offers a one-click
retry that re-runs the host/join flow from the top:

- ErrorTranslator handles HttpRequestException (network down),
  TaskCanceledException (timeout), SocketException (firewall),
  UnauthorizedAccessException + IOException "in use" (Rec Room
  open), missing folder / files (re-pick install), cloudflared
  download truncation (suggests LAN mode), port 5005 already bound
- Patcher log output goes through TranslateMessage which catches
  the same "access denied" / missing files cases
- HostStepsPanel / JoinStepsPanel gain a Retry + "Get help" row
  below the steps; visible only on failure
- "Get help" opens README.md#troubleshooting on GitHub
- Stop / new run hides the retry row so a fresh start is clean
This commit is contained in:
Alexa
2026-05-27 11:17:39 +03:00
parent 7c974e39ab
commit 5b98886937
3 changed files with 226 additions and 11 deletions
+45 -11
View File
@@ -288,6 +288,7 @@ public partial class MainWindow : Window
HostStatusText.Text = "";
StartupStep? activeStep = null;
HostRetryPanel.Visibility = Visibility.Collapsed;
try
{
activeStep = StartStep(_hostSteps, 0);
@@ -331,7 +332,8 @@ public partial class MainWindow : Window
_state.PhotonAppId, _state.PhotonVoiceAppId, _state.PhotonRegion, _hostApex);
if (!patch.Ok)
{
FailStep(activeStep, "Patcher failed: " + Truncate(patch.Log, 300));
FailStepWithFriendly(activeStep, ErrorTranslator.TranslateMessage(patch.Log));
ShowHostRetry();
return;
}
CompleteStep(activeStep);
@@ -356,13 +358,26 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
if (activeStep is not null) FailStep(activeStep, ex.Message);
else HostStatusText.Text = ex.Message;
var friendly = ErrorTranslator.Translate(ex);
if (activeStep is not null) FailStepWithFriendly(activeStep, friendly);
else HostStatusText.Text = $"{friendly.Title} {friendly.Explanation}";
ShowHostRetry();
HostStartBtn.IsEnabled = true;
HostStopBtn.Visibility = Visibility.Collapsed;
}
}
private void ShowHostRetry()
{
HostRetryPanel.Visibility = Visibility.Visible;
}
private void OnHostRetry(object sender, RoutedEventArgs e)
{
HostRetryPanel.Visibility = Visibility.Collapsed;
OnHostStart(sender, e);
}
// ── Step helpers ────────────────────────────────────────────────────
private static StartupStep StartStep(ObservableCollection<StartupStep> steps, int index)
{
@@ -377,11 +392,11 @@ public partial class MainWindow : Window
step.Progress = null;
}
private static void FailStep(StartupStep step, string detail)
private static void FailStepWithFriendly(StartupStep step, FriendlyError err)
{
step.State = StepState.Failed;
step.Progress = null;
step.Detail = detail;
step.Detail = $"{err.Title} {err.Explanation}";
}
private static void UpdateStepProgress(StartupStep step, DownloadProgress p)
@@ -411,6 +426,7 @@ public partial class MainWindow : Window
HostStepsList.ItemsSource = null;
_hostSteps = null;
HostStatusText.Text = "";
HostRetryPanel.Visibility = Visibility.Collapsed;
}
private async Task ShutdownServerAsync()
@@ -503,6 +519,7 @@ public partial class MainWindow : Window
JoinStepsList.ItemsSource = _joinSteps;
JoinStepsPanel.Visibility = Visibility.Visible;
JoinStatusText.Text = "";
JoinRetryPanel.Visibility = Visibility.Collapsed;
StartupStep? activeStep = null;
try
@@ -518,7 +535,8 @@ public partial class MainWindow : Window
payload.PhotonAppId, payload.PhotonVoiceAppId, payload.PhotonRegion, payload.Host);
if (!patch.Ok)
{
FailStep(activeStep, "Patcher failed: " + Truncate(patch.Log, 300));
FailStepWithFriendly(activeStep, ErrorTranslator.TranslateMessage(patch.Log));
JoinRetryPanel.Visibility = Visibility.Visible;
return;
}
CompleteStep(activeStep);
@@ -530,12 +548,31 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
if (activeStep is not null) FailStep(activeStep, ex.Message);
else JoinStatusText.Text = ex.Message;
var friendly = ErrorTranslator.Translate(ex);
if (activeStep is not null) FailStepWithFriendly(activeStep, friendly);
else JoinStatusText.Text = $"{friendly.Title} {friendly.Explanation}";
JoinRetryPanel.Visibility = Visibility.Visible;
}
finally { RefreshJoinReadiness(); }
}
private void OnJoinRetry(object sender, RoutedEventArgs e)
{
JoinRetryPanel.Visibility = Visibility.Collapsed;
OnJoinApply(sender, e);
}
private void OnOpenTroubleshooting(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo(
"https://github.com/DorkSquadRR/DorkNet/blob/main/README.md#troubleshooting")
{ UseShellExecute = true });
}
catch (Exception ex) { ShowError("Couldn't open help: " + ex.Message); }
}
private void OnLaunchRecRoom(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_state.RecRoomPath))
@@ -545,9 +582,6 @@ public partial class MainWindow : Window
}
// ── Helpers ─────────────────────────────────────────────────────────
private static string Truncate(string s, int max)
=> s.Length <= max ? s : s[..max] + "...";
private void ShowError(string message)
=> System.Windows.MessageBox.Show(this, message, "DorkNet",
MessageBoxButton.OK, MessageBoxImage.Warning);