Skip to content

Commit 9d13f03

Browse files
committed
report store improved
1 parent db6c275 commit 9d13f03

1 file changed

Lines changed: 101 additions & 34 deletions

File tree

Basis/Packages/com.basis.framework/Networking/BasisCrashReportStore.cs

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@
1414
///
1515
/// Layout under <c>Application.persistentDataPath/CrashReports</c>:
1616
/// session.active : present while a session is running; deleted on a clean quit.
17-
/// pending.jsonl : one base64-delimited record per captured report this session.
18-
/// If <c>session.active</c> still exists at startup, the previous run did not exit cleanly,
19-
/// so <c>pending.jsonl</c> from that run is loaded for replay before being rotated out.
17+
/// pending.jsonl : reports captured during the CURRENT session.
18+
/// replay.jsonl : an outbox of reports carried over from a crashed session, awaiting
19+
/// send. It is deleted only once the reports have actually been handed off
20+
/// for sending, so a crash before that point simply retries next launch,
21+
/// while a successful send removes the outbox so the same reports are
22+
/// never sent again on a later reboot.
2023
///
21-
/// Records are stored one-per-line as <c>severity \t base64(system) \t base64(message) \t
22-
/// base64(stack)</c> — base64 keeps newlines and arbitrary characters from breaking the
23-
/// line format without pulling in a JSON dependency.
24+
/// Records are stored one per line as <c>severity \t base64(system) \t base64(message) \t
25+
/// base64(stack)</c> — base64 keeps newlines/arbitrary characters from breaking the line
26+
/// format without pulling in a JSON dependency.
2427
/// </summary>
2528
public static class BasisCrashReportStore
2629
{
2730
private const int MaxReplayEntries = 50;
28-
private const long MaxPendingBytes = 2L * 1024 * 1024;
31+
private const long MaxFileBytes = 2L * 1024 * 1024;
2932
private const int MaxMessageChars = 2000;
3033
private const int MaxStackChars = 12000;
3134

@@ -34,6 +37,7 @@ public static class BasisCrashReportStore
3437

3538
private static string _markerPath;
3639
private static string _pendingPath;
40+
private static string _replayPath;
3741
private static bool _initialized;
3842
private static bool _replayed;
3943

@@ -54,17 +58,34 @@ private static void Initialize()
5458
string dir = Path.Combine(Application.persistentDataPath, "CrashReports");
5559
_markerPath = Path.Combine(dir, "session.active");
5660
_pendingPath = Path.Combine(dir, "pending.jsonl");
61+
_replayPath = Path.Combine(dir, "replay.jsonl");
5762
Directory.CreateDirectory(dir);
5863

59-
// Marker survived from last time → the previous session crashed. Grab its reports.
60-
if (File.Exists(_markerPath) && File.Exists(_pendingPath))
64+
lock (FileLock)
6165
{
62-
LoadPrevious();
63-
}
66+
bool previousCrashed = File.Exists(_markerPath);
67+
68+
// A crashed session's freshly-captured reports move into the outbox so they
69+
// survive even if THIS session also crashes before it can send them.
70+
if (previousCrashed && File.Exists(_pendingPath))
71+
{
72+
FoldPendingIntoReplay();
73+
}
6474

65-
// Rotate: start a fresh pending log and (re)arm the session marker.
66-
try { if (File.Exists(_pendingPath)) File.Delete(_pendingPath); } catch { }
67-
try { File.WriteAllText(_markerPath, DateTime.UtcNow.ToString("o")); } catch { }
75+
// The current session always starts with an empty pending log.
76+
TryDelete(_pendingPath);
77+
78+
// Load whatever is still waiting to be sent — this boot's fold, plus anything a
79+
// previous boot loaded but never managed to send. Independent of the marker, so
80+
// an outbox that survived a clean (but offline) shutdown still gets retried.
81+
if (File.Exists(_replayPath))
82+
{
83+
LoadReplay();
84+
}
85+
86+
// (Re)arm the marker for this session.
87+
try { File.WriteAllText(_markerPath, DateTime.UtcNow.ToString("o")); } catch { }
88+
}
6889

6990
Application.quitting += OnQuit;
7091
BasisNetworkModeration.OnCrashReportingStateChanged += OnCrashReportingStateChanged;
@@ -77,11 +98,13 @@ private static void Initialize()
7798

7899
private static void OnQuit()
79100
{
80-
// Clean shutdown → nothing to report next time.
101+
// Clean shutdown → drop this session's pending captures (they were sent live, or
102+
// intentionally not sent). The outbox (replay.jsonl) is deliberately left alone: if it
103+
// still holds unsent reports they should go out on the next launch.
81104
lock (FileLock)
82105
{
83-
try { if (_pendingPath != null && File.Exists(_pendingPath)) File.Delete(_pendingPath); } catch { }
84-
try { if (_markerPath != null && File.Exists(_markerPath)) File.Delete(_markerPath); } catch { }
106+
TryDelete(_pendingPath);
107+
TryDelete(_markerPath);
85108
}
86109
}
87110

@@ -90,10 +113,7 @@ private static void OnCrashReportingStateChanged(bool enabled)
90113
if (enabled) TryReplay();
91114
}
92115

93-
/// <summary>
94-
/// Append one captured report to the pending log. Thread-safe — called from Unity's
95-
/// threaded log callback. Best-effort; IO failures are swallowed.
96-
/// </summary>
116+
/// <summary>Append one captured report to the current session's pending log. Thread-safe.</summary>
97117
public static void Persist(byte severity, string system, string message, string stackTrace)
98118
{
99119
if (!_initialized || string.IsNullOrEmpty(_pendingPath)) return;
@@ -105,22 +125,18 @@ public static void Persist(byte severity, string system, string message, string
105125
+ "\t" + Encode(Truncate(stackTrace, MaxStackChars));
106126
lock (FileLock)
107127
{
108-
// Don't let an exception storm fill the disk.
109-
try
110-
{
111-
FileInfo info = new FileInfo(_pendingPath);
112-
if (info.Exists && info.Length > MaxPendingBytes) return;
113-
}
114-
catch { }
128+
if (OverSizeLimit(_pendingPath)) return; // don't let an exception storm fill the disk
115129
File.AppendAllText(_pendingPath, line + "\n");
116130
}
117131
}
118132
catch { }
119133
}
120134

121135
/// <summary>
122-
/// Send any reports captured from a previous crashed session, once connected and the
123-
/// server allows reporting. Runs at most once per launch.
136+
/// Send any reports carried over from a previous crashed session, once connected and the
137+
/// server allows reporting. Runs at most once per launch; the on-disk outbox is deleted as
138+
/// soon as the reports are taken for sending, so the same reports are never re-sent on a
139+
/// later reboot (and a crash before this point simply retries the unchanged outbox).
124140
/// </summary>
125141
public static void TryReplay()
126142
{
@@ -131,9 +147,18 @@ public static void TryReplay()
131147
List<Entry> toSend;
132148
lock (FileLock)
133149
{
134-
if (_previous.Count == 0) { _replayed = true; return; }
150+
if (_previous.Count == 0)
151+
{
152+
// Nothing to replay — clear any stale outbox and stop checking.
153+
TryDelete(_replayPath);
154+
_replayed = true;
155+
return;
156+
}
135157
toSend = new List<Entry>(_previous);
136158
_previous.Clear();
159+
// Mark as handled: deleting the outbox here is what guarantees these reports are
160+
// not picked up and sent again after the next reboot.
161+
TryDelete(_replayPath);
137162
}
138163
_replayed = true;
139164

@@ -144,11 +169,38 @@ public static void TryReplay()
144169
BasisDebug.Log($"Replayed {toSend.Count} crash report(s) from the previous session.", BasisDebug.LogTag.Networking);
145170
}
146171

147-
private static void LoadPrevious()
172+
// Append pending.jsonl onto replay.jsonl (capped to the most recent entries), so
173+
// carried-over reports accumulate in the outbox. Caller holds FileLock.
174+
private static void FoldPendingIntoReplay()
175+
{
176+
try
177+
{
178+
List<string> lines = new List<string>();
179+
if (File.Exists(_replayPath)) lines.AddRange(File.ReadAllLines(_replayPath));
180+
if (File.Exists(_pendingPath)) lines.AddRange(File.ReadAllLines(_pendingPath));
181+
182+
if (lines.Count > MaxReplayEntries)
183+
lines.RemoveRange(0, lines.Count - MaxReplayEntries);
184+
185+
StringBuilder sb = new StringBuilder();
186+
foreach (string l in lines)
187+
{
188+
if (!string.IsNullOrEmpty(l)) sb.Append(l).Append('\n');
189+
}
190+
File.WriteAllText(_replayPath, sb.ToString());
191+
}
192+
catch (Exception e)
193+
{
194+
BasisDebug.LogWarning($"Failed to fold pending crash reports into the outbox: {e.Message}");
195+
}
196+
}
197+
198+
// Caller holds FileLock.
199+
private static void LoadReplay()
148200
{
149201
try
150202
{
151-
string[] lines = File.ReadAllLines(_pendingPath);
203+
string[] lines = File.ReadAllLines(_replayPath);
152204
int start = Math.Max(0, lines.Length - MaxReplayEntries);
153205
for (int i = start; i < lines.Length; i++)
154206
{
@@ -157,10 +209,25 @@ private static void LoadPrevious()
157209
}
158210
catch (Exception e)
159211
{
160-
BasisDebug.LogWarning($"Failed to load previous crash reports: {e.Message}");
212+
BasisDebug.LogWarning($"Failed to load carried-over crash reports: {e.Message}");
161213
}
162214
}
163215

216+
private static bool OverSizeLimit(string path)
217+
{
218+
try
219+
{
220+
FileInfo info = new FileInfo(path);
221+
return info.Exists && info.Length > MaxFileBytes;
222+
}
223+
catch { return false; }
224+
}
225+
226+
private static void TryDelete(string path)
227+
{
228+
try { if (!string.IsNullOrEmpty(path) && File.Exists(path)) File.Delete(path); } catch { }
229+
}
230+
164231
private static bool TryParse(string line, out Entry entry)
165232
{
166233
entry = default;

0 commit comments

Comments
 (0)