mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-10-01 05:28:09 +02:00
(feature): show unhandled UI errors in a copyable dialog and keep the desktop shortcut on reinstall
This commit is contained in:
parent
296e8c1a47
commit
bacaec389e
10
CHANGELOG.md
10
CHANGELOG.md
@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-09-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Unhandled UI errors open a dialog with the exception details and a **Copy** button. The same text is saved under `%AppData%\MaksIT\UScheduler\logs`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Windows setup keeps an existing desktop shortcut when the checkbox is off on reinstall, and removes a leftover duplicate (`UScheduler (2).lnk`) so a checked box does not leave two icons.
|
||||||
|
|
||||||
## [1.2.0] - 2026-09-21
|
## [1.2.0] - 2026-09-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 25.1%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 30.3%">
|
||||||
<title>Branch Coverage: 25.1%</title>
|
<title>Branch Coverage: 30.3%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">25.1%</text>
|
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">30.3%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">25.1%</text>
|
<text x="128.75" y="14" fill="#fff">30.3%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,21 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 30%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 34.3%">
|
||||||
<title>Line Coverage: 30%</title>
|
<title>Line Coverage: 34.3%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="r">
|
<clipPath id="r">
|
||||||
<rect width="134.5" height="20" rx="3" fill="#fff"/>
|
<rect width="137" height="20" rx="3" fill="#fff"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<g clip-path="url(#r)">
|
<g clip-path="url(#r)">
|
||||||
<rect width="94.5" height="20" fill="#555"/>
|
<rect width="94.5" height="20" fill="#555"/>
|
||||||
<rect x="94.5" width="40" height="20" fill="#dfb317"/>
|
<rect x="94.5" width="42.5" height="20" fill="#dfb317"/>
|
||||||
<rect width="134.5" height="20" fill="url(#s)"/>
|
<rect width="137" height="20" fill="url(#s)"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||||
<text aria-hidden="true" x="114.5" y="15" fill="#010101" fill-opacity=".3">30%</text>
|
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">34.3%</text>
|
||||||
<text x="114.5" y="14" fill="#fff">30%</text>
|
<text x="115.75" y="14" fill="#fff">34.3%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 46.3%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 51.3%">
|
||||||
<title>Method Coverage: 46.3%</title>
|
<title>Method Coverage: 51.3%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">46.3%</text>
|
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">51.3%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">46.3%</text>
|
<text x="128.75" y="14" fill="#fff">51.3%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -39,6 +39,12 @@
|
|||||||
<display_length compare="ge">768</display_length>
|
<display_length compare="ge">768</display_length>
|
||||||
</requires>
|
</requires>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.3.0" date="2026-09-30">
|
||||||
|
<url type="details">https://github.com/MAKS-IT-COM/uscheduler/blob/v1.3.0/CHANGELOG.md</url>
|
||||||
|
<description>
|
||||||
|
<p>Unhandled UI errors open a copyable dialog and are saved to a crash log. Windows setup keeps an existing desktop shortcut on reinstall.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.2.0" date="2026-09-21">
|
<release version="1.2.0" date="2026-09-21">
|
||||||
<url type="details">https://github.com/MAKS-IT-COM/uscheduler/blob/v1.2.0/CHANGELOG.md</url>
|
<url type="details">https://github.com/MAKS-IT-COM/uscheduler/blob/v1.2.0/CHANGELOG.md</url>
|
||||||
<description>
|
<description>
|
||||||
|
|||||||
78
src/MaksIT.UScheduler.Shared/ErrorReport.cs
Normal file
78
src/MaksIT.UScheduler.Shared/ErrorReport.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using MaksIT.UScheduler.Shared.Helpers;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Formats unhandled exceptions for a copyable dialog and a crash log file.</summary>
|
||||||
|
public static class ErrorReport {
|
||||||
|
public static string Capture(Exception exception) {
|
||||||
|
ArgumentNullException.ThrowIfNull(exception);
|
||||||
|
var body = Format(exception);
|
||||||
|
var path = TryWrite(body);
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return body;
|
||||||
|
return body + Environment.NewLine + Environment.NewLine + "Log: " + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Format(Exception exception) {
|
||||||
|
ArgumentNullException.ThrowIfNull(exception);
|
||||||
|
var text = new StringBuilder();
|
||||||
|
text.AppendLine("UScheduler " + Version());
|
||||||
|
text.AppendLine("MaksIT");
|
||||||
|
text.AppendLine(DateTimeOffset.UtcNow.ToString("u"));
|
||||||
|
text.AppendLine(RuntimeInformation.OSDescription);
|
||||||
|
text.AppendLine(RuntimeInformation.FrameworkDescription);
|
||||||
|
text.AppendLine((Environment.Is64BitProcess ? "64-bit" : "32-bit") + " process");
|
||||||
|
text.AppendLine();
|
||||||
|
AppendException(text, exception);
|
||||||
|
return text.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? TryWrite(string report) {
|
||||||
|
try {
|
||||||
|
var dir = UserSettingsPath.LogsDirectory(HostPaths.ProductFolder);
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var name = "crash-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss") + "-" + Environment.ProcessId + ".txt";
|
||||||
|
var path = Path.Combine(dir, name);
|
||||||
|
File.WriteAllText(path, report ?? "");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Version() {
|
||||||
|
var version = Assembly.GetEntryAssembly()?.GetName().Version;
|
||||||
|
return version is null ? "" : version.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendException(StringBuilder text, Exception exception) {
|
||||||
|
var seen = new HashSet<Exception>();
|
||||||
|
var current = exception;
|
||||||
|
var depth = 0;
|
||||||
|
while (current is not null && seen.Add(current)) {
|
||||||
|
if (depth > 0)
|
||||||
|
text.AppendLine().AppendLine("--- inner ---");
|
||||||
|
text.AppendLine(current.GetType().FullName);
|
||||||
|
text.AppendLine(current.Message);
|
||||||
|
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||||
|
text.AppendLine(current.StackTrace);
|
||||||
|
if (current is AggregateException aggregate) {
|
||||||
|
foreach (var inner in aggregate.InnerExceptions) {
|
||||||
|
if (inner is null || ReferenceEquals(inner, current.InnerException) || !seen.Add(inner))
|
||||||
|
continue;
|
||||||
|
text.AppendLine().AppendLine("--- aggregate ---");
|
||||||
|
text.AppendLine(inner.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.InnerException;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,13 @@ public static class UserSettingsPath {
|
|||||||
public static string Get(string product, string fileName = "settings.json") =>
|
public static string Get(string product, string fileName = "settings.json") =>
|
||||||
Combine(Environment.SpecialFolder.ApplicationData, product, fileName);
|
Combine(Environment.SpecialFolder.ApplicationData, product, fileName);
|
||||||
|
|
||||||
|
public static string LogsDirectory(string product) =>
|
||||||
|
Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
Manufacturer,
|
||||||
|
product,
|
||||||
|
"logs");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Machine-wide settings shared by every user and the Windows service
|
/// Machine-wide settings shared by every user and the Windows service
|
||||||
/// (LocalSystem). On Windows this is all-users AppData:
|
/// (LocalSystem). On Windows this is all-users AppData:
|
||||||
|
|||||||
40
src/MaksIT.UScheduler.Tests/ErrorReportTests.cs
Normal file
40
src/MaksIT.UScheduler.Tests/ErrorReportTests.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.UScheduler.Tests;
|
||||||
|
|
||||||
|
public class ErrorReportTests {
|
||||||
|
[Fact]
|
||||||
|
public void Format_IncludesTypeMessageInnerAndStack() {
|
||||||
|
Exception thrown;
|
||||||
|
try {
|
||||||
|
throw new InvalidOperationException("outer", new ArgumentException("inner"));
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
thrown = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = ErrorReport.Format(thrown);
|
||||||
|
Assert.Contains("UScheduler", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("MaksIT", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("InvalidOperationException", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("outer", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("--- inner ---", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("ArgumentException", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("inner", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(nameof(Format_IncludesTypeMessageInnerAndStack), text, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Format_IncludesAggregateInners() {
|
||||||
|
var error = new AggregateException(
|
||||||
|
"batch",
|
||||||
|
new InvalidOperationException("one"),
|
||||||
|
new ArgumentException("two"));
|
||||||
|
var text = ErrorReport.Format(error);
|
||||||
|
Assert.Contains("AggregateException", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("one", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("two", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("--- aggregate ---", text, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/MaksIT.UScheduler.UI/Dialogs/ErrorDialog.cs
Normal file
146
src/MaksIT.UScheduler.UI/Dialogs/ErrorDialog.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using MaksIT.UScheduler.Shared;
|
||||||
|
using MaksIT.UScheduler.UI.ViewModels;
|
||||||
|
using MaksIT.UScheduler.UI.Windows;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.UScheduler.UI.Dialogs;
|
||||||
|
|
||||||
|
|
||||||
|
internal static class ErrorDialog {
|
||||||
|
private static int _open;
|
||||||
|
|
||||||
|
public static void Report(Exception? exception) =>
|
||||||
|
Present(exception, wait: false);
|
||||||
|
|
||||||
|
public static void ReportBlocking(Exception? exception) =>
|
||||||
|
Present(exception, wait: true);
|
||||||
|
|
||||||
|
private static void Present(Exception? exception, bool wait) {
|
||||||
|
if (exception is null)
|
||||||
|
return;
|
||||||
|
var report = ErrorReport.Capture(exception);
|
||||||
|
var dispatcher = TryDispatcher();
|
||||||
|
if (dispatcher is null) {
|
||||||
|
if (wait)
|
||||||
|
ShowStandalone(report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatcher.CheckAccess()) {
|
||||||
|
if (wait)
|
||||||
|
ShowUntilClosed(report);
|
||||||
|
else
|
||||||
|
_ = ShowAsync(report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wait)
|
||||||
|
dispatcher.Invoke(() => ShowUntilClosed(report));
|
||||||
|
else
|
||||||
|
dispatcher.Post(() => _ = ShowAsync(report));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dispatcher? TryDispatcher() {
|
||||||
|
try {
|
||||||
|
return Dispatcher.UIThread;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ShowAsync(string report) {
|
||||||
|
if (Interlocked.Exchange(ref _open, 1) != 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
var window = Create(report);
|
||||||
|
var owner = ActiveWindow();
|
||||||
|
if (owner is { IsVisible: true }) {
|
||||||
|
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
await window.ShowDialog(owner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||||
|
var closed = new TaskCompletionSource();
|
||||||
|
window.Closed += (_, _) => closed.TrySetResult();
|
||||||
|
window.Show();
|
||||||
|
await closed.Task;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Interlocked.Exchange(ref _open, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowUntilClosed(string report) {
|
||||||
|
if (Interlocked.Exchange(ref _open, 1) != 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
var window = Create(report);
|
||||||
|
var owner = ActiveWindow();
|
||||||
|
var closed = false;
|
||||||
|
window.Closed += (_, _) => closed = true;
|
||||||
|
if (owner is { IsVisible: true }) {
|
||||||
|
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
window.Show(owner);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
var dispatcher = Dispatcher.UIThread;
|
||||||
|
while (!closed)
|
||||||
|
dispatcher.RunJobs();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Interlocked.Exchange(ref _open, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowStandalone(string report) {
|
||||||
|
if (Interlocked.Exchange(ref _open, 1) != 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
AppBuilder.Configure<Application>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.AfterSetup(builder => {
|
||||||
|
if (builder.Instance?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime life)
|
||||||
|
return;
|
||||||
|
life.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||||
|
life.MainWindow = Create(report);
|
||||||
|
})
|
||||||
|
.StartWithClassicDesktopLifetime([]);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Interlocked.Exchange(ref _open, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ErrorWindow Create(string report) =>
|
||||||
|
new() {
|
||||||
|
DataContext = new ErrorReportViewModel(report),
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterScreen
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Window? ActiveWindow() {
|
||||||
|
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime life)
|
||||||
|
return null;
|
||||||
|
foreach (var window in life.Windows) {
|
||||||
|
if (window.IsActive && window is not ErrorWindow)
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
return life.MainWindow is ErrorWindow ? null : life.MainWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
||||||
<Version>1.2.0</Version>
|
<Version>1.3.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
|
|||||||
@ -1,18 +1,48 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Logging;
|
using Avalonia.Logging;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using MaksIT.UScheduler.UI.Dialogs;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.UScheduler.UI;
|
namespace MaksIT.UScheduler.UI;
|
||||||
|
|
||||||
internal static class Program {
|
internal static class Program {
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args) =>
|
public static void Main(string[] args) {
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandled;
|
||||||
|
TaskScheduler.UnobservedTaskException += OnUnobservedTask;
|
||||||
|
try {
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
ErrorDialog.ReportBlocking(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Linux: X11/XWayland. Avalonia 12.1.2 native Wayland still hangs on GNOME's
|
// Linux: X11/XWayland. Avalonia 12.1.2 native Wayland still hangs on GNOME's
|
||||||
// xdg_toplevel.configure(0, 0) and never maps a window (GNOME app icon).
|
// xdg_toplevel.configure(0, 0) and never maps a window (GNOME app icon).
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<App>()
|
AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.LogToTrace(LogEventLevel.Warning);
|
.LogToTrace(LogEventLevel.Warning)
|
||||||
|
.AfterSetup(_ => Dispatcher.UIThread.UnhandledException += OnDispatcherUnhandled);
|
||||||
|
|
||||||
|
private static void OnDispatcherUnhandled(object? sender, DispatcherUnhandledExceptionEventArgs e) {
|
||||||
|
e.Handled = true;
|
||||||
|
ErrorDialog.Report(e.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnDomainUnhandled(object? sender, UnhandledExceptionEventArgs e) {
|
||||||
|
if (e.ExceptionObject is not Exception ex)
|
||||||
|
return;
|
||||||
|
if (e.IsTerminating)
|
||||||
|
ErrorDialog.ReportBlocking(ex);
|
||||||
|
else
|
||||||
|
ErrorDialog.Report(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnUnobservedTask(object? sender, UnobservedTaskExceptionEventArgs e) {
|
||||||
|
e.SetObserved();
|
||||||
|
ErrorDialog.Report(e.Exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,12 @@
|
|||||||
<Setter Property="Foreground" Value="#9aa0a6" />
|
<Setter Property="Foreground" Value="#9aa0a6" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="TextBox.error-dump">
|
||||||
|
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Ubuntu Mono, monospace" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="Background" Value="#0E1620" />
|
||||||
|
<Setter Property="Foreground" Value="#E8F4FC" />
|
||||||
|
</Style>
|
||||||
<Style Selector="TextBlock.warning">
|
<Style Selector="TextBlock.warning">
|
||||||
<Setter Property="Foreground" Value="#ff4d6d" />
|
<Setter Property="Foreground" Value="#ff4d6d" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
|||||||
19
src/MaksIT.UScheduler.UI/ViewModels/ErrorReportViewModel.cs
Normal file
19
src/MaksIT.UScheduler.UI/ViewModels/ErrorReportViewModel.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.UScheduler.UI.ViewModels;
|
||||||
|
|
||||||
|
|
||||||
|
public sealed partial class ErrorReportViewModel : ObservableObject {
|
||||||
|
public ErrorReportViewModel(string report) {
|
||||||
|
Report = report ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Report { get; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string copyStatus = "";
|
||||||
|
|
||||||
|
public void MarkCopied() =>
|
||||||
|
CopyStatus = "Copied";
|
||||||
|
}
|
||||||
47
src/MaksIT.UScheduler.UI/Windows/ErrorWindow.axaml
Normal file
47
src/MaksIT.UScheduler.UI/Windows/ErrorWindow.axaml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:MaksIT.UScheduler.UI.ViewModels"
|
||||||
|
x:Class="MaksIT.UScheduler.UI.Windows.ErrorWindow"
|
||||||
|
x:DataType="vm:ErrorReportViewModel"
|
||||||
|
Width="720"
|
||||||
|
Height="520"
|
||||||
|
MinWidth="480"
|
||||||
|
MinHeight="360"
|
||||||
|
CanResize="True"
|
||||||
|
ShowInTaskbar="True"
|
||||||
|
Topmost="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="UScheduler">
|
||||||
|
<DockPanel Margin="16">
|
||||||
|
<StackPanel DockPanel.Dock="Bottom"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Spacing="8"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<TextBlock Text="{Binding CopyStatus}"
|
||||||
|
Classes="hint"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,8,0" />
|
||||||
|
<Button Content="Copy"
|
||||||
|
Click="OnCopyClick" />
|
||||||
|
<Button Content="Close"
|
||||||
|
Padding="16,8"
|
||||||
|
Click="OnCloseClick"
|
||||||
|
IsDefault="True"
|
||||||
|
IsCancel="True" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top"
|
||||||
|
Text="Something went wrong. The details below are also saved to a crash log."
|
||||||
|
Classes="hint"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,0,0,12" />
|
||||||
|
<TextBox Text="{Binding Report}"
|
||||||
|
Classes="error-dump"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto" />
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
||||||
31
src/MaksIT.UScheduler.UI/Windows/ErrorWindow.axaml.cs
Normal file
31
src/MaksIT.UScheduler.UI/Windows/ErrorWindow.axaml.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using MaksIT.UScheduler.UI.ViewModels;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.UScheduler.UI.Windows;
|
||||||
|
|
||||||
|
|
||||||
|
public partial class ErrorWindow : Window {
|
||||||
|
public ErrorWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnCopyClick(object? sender, RoutedEventArgs e) {
|
||||||
|
if (DataContext is not ErrorReportViewModel vm)
|
||||||
|
return;
|
||||||
|
var clipboard = Clipboard;
|
||||||
|
if (clipboard is null)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
await clipboard.SetTextAsync(vm.Report);
|
||||||
|
vm.MarkCopied();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCloseClick(object? sender, RoutedEventArgs e) =>
|
||||||
|
Close();
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
<Version>1.2.0</Version>
|
<Version>1.3.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
||||||
|
|||||||
@ -323,8 +323,33 @@ function New-WixPackageXml {
|
|||||||
$desktopProp = $xml.CreateElement('Property', $ns)
|
$desktopProp = $xml.CreateElement('Property', $ns)
|
||||||
$null = $desktopProp.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT')
|
$null = $desktopProp.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT')
|
||||||
$null = $desktopProp.SetAttribute('Value', '0')
|
$null = $desktopProp.SetAttribute('Value', '0')
|
||||||
|
$null = $desktopProp.SetAttribute('Secure', 'yes')
|
||||||
$null = $package.AppendChild($desktopProp)
|
$null = $package.AppendChild($desktopProp)
|
||||||
|
|
||||||
|
# Present when a previous install wrote the desktop shortcut marker.
|
||||||
|
# Burn also searches HKCU as the user and passes DESKTOPSHORTCUTFOUND,
|
||||||
|
# because a per-machine execute sequence runs as SYSTEM.
|
||||||
|
$foundProp = $xml.CreateElement('Property', $ns)
|
||||||
|
$null = $foundProp.SetAttribute('Id', 'DESKTOPSHORTCUTFOUND')
|
||||||
|
$null = $foundProp.SetAttribute('Secure', 'yes')
|
||||||
|
$foundSearch = $xml.CreateElement('RegistrySearch', $ns)
|
||||||
|
$null = $foundSearch.SetAttribute('Id', 'DesktopShortcutFoundSearch')
|
||||||
|
$null = $foundSearch.SetAttribute('Root', 'HKCU')
|
||||||
|
$null = $foundSearch.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
||||||
|
$null = $foundSearch.SetAttribute('Name', 'desktop')
|
||||||
|
$null = $foundSearch.SetAttribute('Type', 'raw')
|
||||||
|
$null = $foundProp.AppendChild($foundSearch)
|
||||||
|
$null = $package.AppendChild($foundProp)
|
||||||
|
|
||||||
|
# Checkbox off on reinstall must not drop an icon that is already there.
|
||||||
|
# Major upgrade removes the previous shortcut; this installs that one again.
|
||||||
|
$keepShortcut = $xml.CreateElement('SetProperty', $ns)
|
||||||
|
$null = $keepShortcut.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT')
|
||||||
|
$null = $keepShortcut.SetAttribute('Value', '1')
|
||||||
|
$null = $keepShortcut.SetAttribute('Before', 'CostFinalize')
|
||||||
|
$null = $keepShortcut.SetAttribute('Condition', 'DESKTOPSHORTCUTFOUND = "1"')
|
||||||
|
$null = $package.AppendChild($keepShortcut)
|
||||||
|
|
||||||
$productFolder = Get-DesktopInstallFolderName `
|
$productFolder = Get-DesktopInstallFolderName `
|
||||||
-AppName $AppName `
|
-AppName $AppName `
|
||||||
-Manufacturer $Manufacturer `
|
-Manufacturer $Manufacturer `
|
||||||
@ -449,7 +474,8 @@ function New-WixPackageXml {
|
|||||||
$desktop = $xml.CreateElement('Component', $ns)
|
$desktop = $xml.CreateElement('Component', $ns)
|
||||||
$null = $desktop.SetAttribute('Id', 'DesktopShortcut')
|
$null = $desktop.SetAttribute('Id', 'DesktopShortcut')
|
||||||
$null = $desktop.SetAttribute('Directory', 'DesktopFolder')
|
$null = $desktop.SetAttribute('Directory', 'DesktopFolder')
|
||||||
$null = $desktop.SetAttribute('Guid', '*')
|
$shortcutGuid = Get-DesktopShortcutComponentGuid -Manufacturer $Manufacturer -AppName $AppName
|
||||||
|
$null = $desktop.SetAttribute('Guid', $shortcutGuid.ToString('D'))
|
||||||
$null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1')
|
$null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1')
|
||||||
$desktopShortcut = $xml.CreateElement('Shortcut', $ns)
|
$desktopShortcut = $xml.CreateElement('Shortcut', $ns)
|
||||||
$null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut')
|
$null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut')
|
||||||
@ -461,6 +487,26 @@ function New-WixPackageXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$null = $desktop.AppendChild($desktopShortcut)
|
$null = $desktop.AppendChild($desktopShortcut)
|
||||||
|
# Same file name is replaced. A second ".lnk" (and the "(2)" copy) is removed
|
||||||
|
# before CreateShortcuts so a checked box does not leave two desktop icons.
|
||||||
|
$cleanupNames = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$cleanupNames.Add($productFolder)
|
||||||
|
if (-not $cleanupNames.Contains($AppName)) {
|
||||||
|
$cleanupNames.Add($AppName)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanupIndex = 0
|
||||||
|
foreach ($shortcutName in $cleanupNames) {
|
||||||
|
foreach ($suffix in @('', ' (2)')) {
|
||||||
|
$cleanupIndex++
|
||||||
|
$removeLnk = $xml.CreateElement('RemoveFile', $ns)
|
||||||
|
$null = $removeLnk.SetAttribute('Id', "DesktopShortcutCleanup$cleanupIndex")
|
||||||
|
$null = $removeLnk.SetAttribute('Name', "$shortcutName$suffix.lnk")
|
||||||
|
$null = $removeLnk.SetAttribute('On', 'install')
|
||||||
|
$null = $desktop.AppendChild($removeLnk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$desktopReg = $xml.CreateElement('RegistryValue', $ns)
|
$desktopReg = $xml.CreateElement('RegistryValue', $ns)
|
||||||
$null = $desktopReg.SetAttribute('Root', 'HKCU')
|
$null = $desktopReg.SetAttribute('Root', 'HKCU')
|
||||||
$null = $desktopReg.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
$null = $desktopReg.SetAttribute('Key', "Software\$Manufacturer\$AppName")
|
||||||
@ -874,6 +920,27 @@ function Assert-FlatpakAppId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-DesktopShortcutComponentGuid {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Manufacturer,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AppName
|
||||||
|
)
|
||||||
|
|
||||||
|
$identity = "maksit-desktop-shortcut|$Manufacturer|$AppName"
|
||||||
|
$md5 = [System.Security.Cryptography.MD5]::Create()
|
||||||
|
try {
|
||||||
|
$hash = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($identity))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$md5.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return [guid]::new($hash)
|
||||||
|
}
|
||||||
|
|
||||||
function Get-DerivedBundleUpgradeCode {
|
function Get-DerivedBundleUpgradeCode {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -1196,15 +1263,18 @@ function New-WixBundleXml {
|
|||||||
$folderRoot + $Manufacturer + '\' + $productFolder
|
$folderRoot + $Manufacturer + '\' + $productFolder
|
||||||
}
|
}
|
||||||
$escapedFolder = [System.Security.SecurityElement]::Escape($folderPath)
|
$escapedFolder = [System.Security.SecurityElement]::Escape($folderPath)
|
||||||
|
$escapedShortcutKey = [System.Security.SecurityElement]::Escape("Software\$Manufacturer\$AppName")
|
||||||
|
|
||||||
# WiX v7 Bundle has no Scope attribute (WIX0004). MSI Package/@Scope plus
|
# WiX v7 Bundle has no Scope attribute (WIX0004). MSI Package/@Scope plus
|
||||||
# InstallFolder tokens decide per-machine vs per-user; Burn infers bundle scope.
|
# InstallFolder tokens decide per-machine vs per-user; Burn infers bundle scope.
|
||||||
return @"
|
return @"
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
|
||||||
<Bundle Name="$escapedName" Manufacturer="$escapedMfr" Version="$ProductVersion" UpgradeCode="$($bundleUpgrade.ToString('D'))"$iconAttr>
|
<Bundle Name="$escapedName" Manufacturer="$escapedMfr" Version="$ProductVersion" UpgradeCode="$($bundleUpgrade.ToString('D'))"$iconAttr>
|
||||||
<Variable Name="InstallFolder" Type="formatted" Value="$escapedFolder" bal:Overridable="yes" />
|
<Variable Name="InstallFolder" Type="formatted" Value="$escapedFolder" bal:Overridable="yes" />
|
||||||
<Variable Name="InstallDesktopShortcut" Type="numeric" Value="0" bal:Overridable="yes" />
|
<Variable Name="InstallDesktopShortcut" Type="numeric" Value="0" bal:Overridable="yes" />
|
||||||
|
<Variable Name="DesktopShortcutFound" Type="string" Value="" bal:Overridable="yes" />
|
||||||
|
<util:RegistrySearch Id="DesktopShortcutFoundSearch" Variable="DesktopShortcutFound" Root="HKCU" Key="$escapedShortcutKey" Value="desktop" Result="exists" />
|
||||||
<BootstrapperApplication>
|
<BootstrapperApplication>
|
||||||
<bal:WixStandardBootstrapperApplication Theme="$theme" LicenseUrl=""$logoAttrs />
|
<bal:WixStandardBootstrapperApplication Theme="$theme" LicenseUrl=""$logoAttrs />
|
||||||
</BootstrapperApplication>
|
</BootstrapperApplication>
|
||||||
@ -1212,6 +1282,7 @@ function New-WixBundleXml {
|
|||||||
<MsiPackage SourceFile="$escapedMsi" Compressed="yes" Vital="yes">
|
<MsiPackage SourceFile="$escapedMsi" Compressed="yes" Vital="yes">
|
||||||
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
||||||
<MsiProperty Name="INSTALLDESKTOPSHORTCUT" Value="[InstallDesktopShortcut]" />
|
<MsiProperty Name="INSTALLDESKTOPSHORTCUT" Value="[InstallDesktopShortcut]" />
|
||||||
|
<MsiProperty Name="DESKTOPSHORTCUTFOUND" Value="[DesktopShortcutFound]" />
|
||||||
</MsiPackage>
|
</MsiPackage>
|
||||||
</Chain>
|
</Chain>
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
|||||||
@ -13,7 +13,8 @@
|
|||||||
per-machine installs go to `C:\Program Files`, not Program Files (x86).
|
per-machine installs go to `C:\Program Files`, not Program Files (x86).
|
||||||
Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the
|
Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the
|
||||||
OSMF EULA (`wix eula accept wix7` or `-acceptEula wix7`) and
|
OSMF EULA (`wix eula accept wix7` or `-acceptEula wix7`) and
|
||||||
`wix extension add -g WixToolset.BootstrapperApplications.wixext`.
|
`wix extension add -g WixToolset.BootstrapperApplications.wixext` and
|
||||||
|
`wix extension add -g WixToolset.Util.wixext`.
|
||||||
Reopen the shell so `%USERPROFILE%\.dotnet\tools` is on PATH.
|
Reopen the shell so `%USERPROFILE%\.dotnet\tools` is on PATH.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ function Test-WixMissingException {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Get-WixMissingInstallMessage {
|
function Get-WixMissingInstallMessage {
|
||||||
return "WiX CLI ('wix') is not on PATH. Install with: dotnet tool install -g wix. For WiX v7 also run: wix eula accept wix7; wix extension add -g WixToolset.BootstrapperApplications.wixext. Reopen the terminal so %USERPROFILE%\.dotnet\tools is on PATH."
|
return "WiX CLI ('wix') is not on PATH. Install with: dotnet tool install -g wix. For WiX v7 also run: wix eula accept wix7; wix extension add -g WixToolset.BootstrapperApplications.wixext; wix extension add -g WixToolset.Util.wixext. Reopen the terminal so %USERPROFILE%\.dotnet\tools is on PATH."
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-WixCliVersionText {
|
function Get-WixCliVersionText {
|
||||||
@ -280,7 +281,7 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Building Windows installer exe ($wixArch)..."
|
Write-Log -Level "STEP" -Message "Building Windows installer exe ($wixArch)..."
|
||||||
try {
|
try {
|
||||||
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($bundleWxsPath, '-ext', $bundleExt, '-o', $exePath)) | Out-Null
|
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($bundleWxsPath, '-ext', $bundleExt, '-ext', 'WixToolset.Util.wixext', '-o', $exePath)) | Out-Null
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
if (Test-WixMissingException -ErrorRecord $_) {
|
if (Test-WixMissingException -ErrorRecord $_) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user