• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ParadoxGameConverters / Fronter.NET / 17702925212

13 Sep 2025 10:36PM UTC coverage: 19.079% (+0.9%) from 18.155%
17702925212

Pull #905

github

web-flow
Merge bd0393206 into edd5b18e7
Pull Request #905: Optimize the slicing of log messages from converter backends

99 of 708 branches covered (13.98%)

Branch coverage included in aggregate %.

52 of 79 new or added lines in 5 files covered. (65.82%)

1 existing line in 1 file now uncovered.

572 of 2809 relevant lines covered (20.36%)

9.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Fronter.NET/Services/ConverterLauncher.cs
1
using Amazon.S3;
2
using Amazon.S3.Transfer;
3
using Avalonia;
4
using Avalonia.Controls.ApplicationLifetimes;
5
using Avalonia.Threading;
6
using commonItems;
7
using Fronter.Extensions;
8
using Fronter.Models.Configuration;
9
using Fronter.Views;
10
using log4net;
11
using log4net.Core;
12
using MsBox.Avalonia;
13
using MsBox.Avalonia.Enums;
14
using System;
15
using System.Collections.Generic;
16
using System.Diagnostics;
17
using System.Globalization;
18
using System.IO;
19
using System.IO.Compression;
20
using System.Linq;
21
using System.Threading.Tasks;
22

23
namespace Fronter.Services;
24

25
internal sealed class ConverterLauncher {
26
        private static readonly ILog logger = LogManager.GetLogger("Converter launcher");
×
27
        private Level? lastLevelFromBackend;
28
        internal ConverterLauncher(Config config) {
×
29
                this.config = config;
×
30
        }
×
31

32
        private string? GetBackendExePathRelativeToFrontend() {
×
33
                var converterFolder = config.ConverterFolder;
×
34
                var backendExePath = config.BackendExePath;
×
35

36
                if (string.IsNullOrEmpty(backendExePath)) {
×
37
                        logger.Error("Converter location has not been set!");
×
38
                        return null;
×
39
                }
40

41
                var extension = CommonFunctions.GetExtension(backendExePath);
×
42
                if (string.IsNullOrEmpty(extension) && OperatingSystem.IsWindows()) {
×
43
                        backendExePath += ".exe";
×
44
                }
×
45
                var backendExePathRelativeToFrontend = Path.Combine(converterFolder, backendExePath);
×
46

47
                return backendExePathRelativeToFrontend;
×
48
        }
×
49

50
        public async Task<bool> LaunchConverter() {
×
51
                var backendExePathRelativeToFrontend = GetBackendExePathRelativeToFrontend();
×
52
                if (backendExePathRelativeToFrontend is null) {
×
53
                        return false;
×
54
                }
55

56
                if (!File.Exists(backendExePathRelativeToFrontend)) {
×
57
                        logger.Error("Could not find converter executable!");
×
58
                        return false;
×
59
                }
60

61
                logger.Debug($"Using {backendExePathRelativeToFrontend} as converter backend...");
×
62
                var startInfo = new ProcessStartInfo {
×
63
                        FileName = backendExePathRelativeToFrontend,
×
64
                        WorkingDirectory = CommonFunctions.GetPath(backendExePathRelativeToFrontend),
×
65
                        CreateNoWindow = true,
×
66
                        UseShellExecute = false,
×
67
                        RedirectStandardOutput = true,
×
68
                        RedirectStandardInput = true,
×
69
                };
×
70
                var extension = CommonFunctions.GetExtension(backendExePathRelativeToFrontend);
×
71
                if (string.Equals(extension, "jar", StringComparison.OrdinalIgnoreCase)) {
×
72
                        startInfo.FileName = "javaw";
×
73
                        startInfo.Arguments = $"-jar {CommonFunctions.TrimPath(backendExePathRelativeToFrontend)}";
×
74
                }
×
75

76
                using Process process = new();
×
77
                process.StartInfo = startInfo;
×
78
                process.OutputDataReceived += (sender, args) => {
×
79
                        var logLine = MessageSlicer.SliceMessage(args.Data ?? string.Empty);
×
80
                        var level = logLine.Level;
×
81
                        if (level is null && string.IsNullOrEmpty(logLine.Message)) {
×
82
                                return;
×
83
                        }
×
84

×
85
                        // Get level to display.
×
86
                        var logLevel = level ?? lastLevelFromBackend ?? Level.Info;
×
87

×
NEW
88
                        logger.LogWithCustomTimestamp(logLine.Timestamp, logLevel, logLine.Message);
×
89

×
90
                        if (level is not null) {
×
91
                                lastLevelFromBackend = level;
×
92
                        }
×
93
                };
×
94

95
                var timer = new Stopwatch();
×
96
                timer.Start();
×
97

98
                process.Start();
×
99
                process.EnableRaisingEvents = true;
×
100
                process.PriorityClass = ProcessPriorityClass.RealTime;
×
101
                process.PriorityBoostEnabled = OperatingSystem.IsWindows();
×
102

103
                process.BeginOutputReadLine();
×
104

105
                // Kill converter backend when frontend is closed.
106
                if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
×
107
                        var processId = process.Id;
×
108
                        desktop.ShutdownRequested += (sender, args) => {
×
109
                                try {
×
110
                                        var backendProcess = Process.GetProcessById(processId);
×
111
                                        logger.Info("Killing converter backend...");
×
112
                                        backendProcess.Kill();
×
113
                                } catch (ArgumentException) {
×
114
                                        // Process already exited.
×
115
                                }
×
116
                        };
×
117
                }
×
118

119
                await process.WaitForExitAsync();
×
120

121
                timer.Stop();
×
122

123
                if (process.ExitCode == 0) {
×
124
                        logger.Info($"Converter exited at {timer.Elapsed.TotalSeconds} seconds.");
×
125
                        return true;
×
126
                }
127

128
                if (process.ExitCode == 1) {
×
129
                        // Exit code 1 is for user errors, so we don't need to send it to Sentry.
130
                        logger.Error($"Converter failed and exited at {timer.Elapsed.TotalSeconds} seconds.");
×
131
                        return false;
×
132
                }
133

134
                if (process.ExitCode == -532462766) {
×
135
                        logger.Error("Converter exited with code -532462766. This is a most likely an antivirus issue.");
×
136
                        logger.Notice("Please add the converter to your antivirus' whitelist.");
×
137
                } else {
×
138
                        logger.Debug($"Converter exit code: {process.ExitCode}");
×
139
                        logger.Error("Converter error! See log.txt for details.");
×
140
                }
×
141

142
                var helpPageOpened = await TryOpenHelpPage(process.ExitCode);
×
143

144
                if (!helpPageOpened && config.SentryDsn is not null) {
×
145
                        var saveUploadConsent = await Dispatcher.UIThread.InvokeAsync(GetSaveUploadConsent);
×
146
                        if (!saveUploadConsent) {
×
147
                                return false;
×
148
                        }
149

150
                        var sentryHelper = new SentryHelper(config);
×
151
                        try {
×
152
                                await AttachLogAndSaveToSentry(config, sentryHelper);
×
153
                        } catch (Exception e) {
×
154
                                var warnMessage = $"Failed to attach log and save to Sentry event: {e.Message}";
×
155
                                logger.Warn(warnMessage);
×
156
                                sentryHelper.AddBreadcrumb(warnMessage);
×
157
                        }
×
158

159
                        try {
×
160
                                sentryHelper.SendMessageToSentry(process.ExitCode);
×
161
                                if (saveUploadConsent) {
×
162
                                        Logger.Notice("Uploaded information about the error, thank you!");
×
163
                                }
×
164
                        } catch (Exception e) {
×
165
                                logger.Warn($"Failed to send message to Sentry: {e.Message}");
×
166
                        }
×
167
                } else {
×
168
                        logger.Error("If you require assistance, please visit the converter's forum thread " +
×
169
                                     "for a detailed postmortem.");
×
170
                }
×
171
                return false;
×
172
        }
×
173

174
        private static async Task<bool> GetSaveUploadConsent() {
×
175
                var saveUploadConsent = await MessageBoxManager.GetMessageBoxStandard(
×
176
                        title: TranslationSource.Instance.Translate("SAVE_UPLOAD_CONSENT_TITLE"),
×
177
                        text: TranslationSource.Instance.Translate("SAVE_UPLOAD_CONSENT_BODY"),
×
178
                        ButtonEnum.OkCancel,
×
179
                        Icon.Question
×
180
                ).ShowWindowDialogAsync(MainWindow.Instance);
×
181
                return saveUploadConsent == ButtonResult.Ok;
×
182
        }
×
183

184
        private static async Task AttachLogAndSaveToSentry(Config config, SentryHelper sentryHelper) {
×
185
                sentryHelper.AddAttachment("log.txt");
×
186

187
                var saveLocation = config.RequiredFiles.FirstOrDefault(f => f.Name.Equals("SaveGame"))?.Value;
×
188
                if (saveLocation is null) {
×
189
                        return;
×
190
                }
191

192
                Directory.CreateDirectory("temp");
×
193

194
                // Create zip with save file.
195
                var dateTimeString = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
×
196
                var asciiSaveName = CommonFunctions.TrimExtension(Path.GetFileName(saveLocation)).FoldToASCII();
×
197
                var archivePath = $"temp/SaveGame_{dateTimeString}_{asciiSaveName}.zip";
×
198
                using (var zip = ZipFile.Open(archivePath, ZipArchiveMode.Create)) {
×
199
                        zip.CreateEntryFromFile(saveLocation, new FileInfo(saveLocation).Name);
×
200
                }
×
201

202
                // Sentry allows up to 20 MB per compressed request.
203
                // So we need to calculate whether we can fit the save archive.
204
                // Otherwise we upload it to Backblaze.
205
                var logSize = new FileInfo("log.txt").Length; // Size in bytes.
×
206
                const int spaceForBaseRequest = 1024 * 1024 / 2; // 0.5 MB, arbitrary.
207
                var saveSizeLimitForSentry = (20 * 1024 * 1024) - (logSize + spaceForBaseRequest);
×
208
                var saveArchiveSize = new FileInfo(archivePath).Length;
×
209
                if (saveArchiveSize <= saveSizeLimitForSentry) {
×
210
                        logger.Debug($"Save file is {saveArchiveSize} bytes, uploading to Sentry.");
×
211
                        sentryHelper.AddAttachment(archivePath);
×
212
                } else {
×
213
                        logger.Debug($"Save file is {saveArchiveSize} bytes, uploading to Backblaze.");
×
214
                        await UploadSaveArchiveToBackblaze(archivePath, sentryHelper);
×
215
                }
×
216
        }
×
217

218
        private static async Task UploadSaveArchiveToBackblaze(string archivePath, SentryHelper sentryHelper) {
×
219
                // Add Backblaze credentials to breadcrumbs for debugging.
220
                var keyId = Secrets.BackblazeKeyId;
×
221
                var applicationKey = Secrets.BackblazeApplicationKey;
×
222
                var bucketId = Secrets.BackblazeBucketId;
×
223
                sentryHelper.AddBreadcrumb($"Backblaze key ID: \"{keyId}\"");
×
224
                sentryHelper.AddBreadcrumb($"Backblaze application key: \"{applicationKey}\"");
×
225
                sentryHelper.AddBreadcrumb($"Backblaze bucket ID: \"{bucketId}\"");
×
226
                sentryHelper.AddBreadcrumb($"Archive name: {Path.GetFileName(archivePath)}");
×
227

228
                var s3Config = new AmazonS3Config {
×
229
                        ServiceURL = "https://s3.eu-central-003.backblazeb2.com",
×
230
                };
×
231

232
                var s3Client = new AmazonS3Client(keyId, applicationKey, s3Config);
×
233
                var fileTransferUtility = new TransferUtility(s3Client);
×
234

235
                try {
×
236
                        await fileTransferUtility.UploadAsync(archivePath, "save-zips");
×
237
                        Logger.Info("Upload completed.");
×
238
                }
×
239
                catch (AmazonS3Exception e) {
×
240
                        string message = $"Error encountered on server. Message:'{e.Message}' when writing an object.";
×
241
                        Logger.Error(message);
×
242
                        sentryHelper.AddBreadcrumb(message);
×
243
                }
×
244
                catch (Exception e) {
×
245
                        string message = $"Unknown encountered on server. Message:'{e.Message}' when writing an object.";
×
246
                        Logger.Error(message);
×
247
                        sentryHelper.AddBreadcrumb(message);
×
248
                }
×
249
        }
×
250

251
        /// <summary>
252
        /// Tries to open a help page based on the converter backend exit code.
253
        /// </summary>
254
        /// <param name="exitCode">Exit code of the converter backend.</param>
255
        /// <returns>true if a help page was opened, otherwise false</returns>
256
        private static async Task<bool> TryOpenHelpPage(int exitCode) {
×
257
                if (OperatingSystem.IsWindows()) {
×
258
                        var exitCodeToHelpDict = new Dictionary<int, string> {
×
259
                                {-1073741790, "https://answers.microsoft.com/en-us/windows/forum/all/the-application-was-unable-to-start-correctly/e06ee08a-26c5-447a-80bd-ed339488d0f3"}, // -1073741790 = 0xC0000022
×
260
                                {-1073741795, "https://ugetfix.com/ask/how-to-fix-file-system-error-1073741795-in-windows/"},
×
261
                                {-532462766, "https://www.thewindowsclub.com/add-file-or-folder-to-antivirus-exception-list-in-windows"},
×
262
                        };
×
263
                        if (!exitCodeToHelpDict.TryGetValue(exitCode, out var helpLink)) {
×
264
                                return false;
×
265
                        }
266

267
                        var msgBoxResult = await Dispatcher.UIThread.InvokeAsync(() => MessageBoxManager.GetMessageBoxStandard(
×
268
                                title: "Fix suggestion",
×
269
                                text: "Would you like to open a help page with instructions on how to fix this issue?",
×
270
                                ButtonEnum.YesNo,
×
271
                                Icon.Info
×
272
                        ).ShowWindowDialogAsync(MainWindow.Instance));
×
273

274
                        if (msgBoxResult == ButtonResult.Yes) {
×
275
                                BrowserLauncher.Open(helpLink);
×
276
                                return true;
×
277
                        }
278
                        Logger.Debug("User declined to open help page.");
×
279
                }
×
280

281
                return false;
×
282
        }
×
283

284
        private readonly Config config;
285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc