Skip to content

Commit e964f7e

Browse files
authored
Merge pull request #11 from betahub-io/ben/fixes
Ben/fixes
2 parents 0932c48 + 40a94aa commit e964f7e

File tree

5 files changed

+436
-39
lines changed

5 files changed

+436
-39
lines changed

CLAUDE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is a Unity plugin for BetaHub bug reporting that enables in-game bug submissions with video recording, screenshot capture, and log collection. The plugin is distributed as a Unity Package Manager (UPM) package.
8+
9+
## Architecture
10+
11+
### Core Components
12+
13+
- **GameRecorder** (`Runtime/Scripts/GameRecorder.cs`): Handles video recording using FFmpeg with optimized GPU-based capture
14+
- **BugReportUI** (`Runtime/Scripts/BugReportUI.cs`): Main UI component for bug report submission form
15+
- **Issue** (`Runtime/Scripts/Issue.cs`): Data model representing bug reports and their upload state
16+
- **VideoEncoder** (`Runtime/Scripts/VideoEncoder.cs`): FFmpeg wrapper for video encoding with segmented recording
17+
- **Process Wrappers**: Platform-specific process handling for FFmpeg
18+
- **IProcessWrapper** (`Runtime/Scripts/IProcessWrapper.cs`): Interface for process abstraction
19+
- **DotNetProcessWrapper** (`Runtime/Scripts/DotNetProcessWrapper.cs`): .NET/Mono implementation
20+
- **NativeProcessWrapper** (`Runtime/Scripts/NativeProcessWrapper.cs`): IL2CPP implementation using native library
21+
22+
### Assembly Definitions
23+
24+
- **Runtime**: `io.betahub.bugreporter.asmdef` - Main plugin runtime assembly
25+
- **Editor**: `io.betahub.bugreporter.editor.asmdef` - Editor-only features (FFmpeg downloader)
26+
27+
### Platform Support
28+
29+
The plugin supports Windows, macOS, and Linux with special handling for IL2CPP builds:
30+
- **Mono/.NET**: Uses standard .NET Process class
31+
- **IL2CPP**: Requires `ENABLE_BETAHUB_FFMPEG` scripting symbol and native process wrapper libraries in `Plugins/` directories
32+
33+
### Native Libraries
34+
35+
Platform-specific native libraries for IL2CPP FFmpeg support:
36+
- Windows: `Plugins/x86_64/betahub_process_wrapper.dll`
37+
- macOS: `Plugins/macOS/libbetahub_process_wrapper.dylib`
38+
- Linux: `Plugins/Linux/x86_64/libbetahub_process_wrapper.so`
39+
40+
## Development
41+
42+
### Unity Package Structure
43+
44+
This project follows Unity Package Manager conventions:
45+
- `package.json`: Package metadata and dependencies
46+
- `Runtime/`: Runtime scripts and assets
47+
- `Editor/`: Editor-only scripts
48+
- `Samples~/`: Sample scenes and scripts
49+
- Assembly definition files (`.asmdef`) organize code into separate assemblies
50+
51+
### Testing
52+
53+
The plugin includes a working demo scene in `Samples~/ExampleScene/` with:
54+
- Sample scene setup
55+
- Example integration with `RotateCube.cs` script
56+
- Pre-configured demo project settings for immediate testing
57+
58+
### FFmpeg Integration
59+
60+
The plugin automatically downloads FFmpeg binaries through the editor script `FfmpegDownloader.cs`. Video recording is implemented with:
61+
- Segmented recording (10-second segments, 60-second rolling window)
62+
- GPU-optimized capture using RenderTextures
63+
- Cross-platform process wrapper abstraction
64+
- Error handling and retry logic for file operations
65+
66+
## Configuration
67+
68+
### Required Settings for IL2CPP
69+
70+
When building with IL2CPP scripting backend:
71+
1. Define `ENABLE_BETAHUB_FFMPEG` in Player Settings > Scripting Define Symbols
72+
2. Ensure native process wrapper libraries are included in build
73+
74+
### Demo Project
75+
76+
The plugin comes pre-configured with demo project credentials (`DEMO_PROJECT_ID = "pr-5287510306"`) for immediate testing. Reports submitted to the demo project are only visible to the submitter via email links.
77+
78+
## Development Notes
79+
80+
- Do not generate meta files, let Unity do this
81+
- Use `ruby validate_prefab.rb <prefab_path>` to validate Unity prefabs for duplicate IDs and broken references

CLAUDE.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Scripts/BugReportUI.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ public class BugReportUI : MonoBehaviour
124124
private List<Issue.LogFileReference> _logFiles = new List<Issue.LogFileReference>();
125125

126126
private static Logger _logger;
127+
128+
public static Logger Logger => _logger;
127129
private bool _cursorStateChanged;
128130
private CursorLockMode _previousCursorLockMode;
129131

@@ -402,7 +404,7 @@ private IEnumerator SubmitBugReportCoroutine()
402404
// skip if size over 200MB
403405
if (new FileInfo(_logger.LogPath).Length < 200 * 1024 * 1024)
404406
{
405-
logFiles.Add(new Issue.LogFileReference { path = _logger.LogPath, removeAfterUpload = false });
407+
logFiles.Add(new Issue.LogFileReference { logger = _logger, removeAfterUpload = false });
406408
}
407409
}
408410
}
@@ -645,6 +647,26 @@ void OnDestroy()
645647
// Cleanup handled automatically since we don't subscribe to events
646648
}
647649

650+
public static void PauseLogger()
651+
{
652+
#if !DISABLE_BETAHUB_LOGGER
653+
if (_logger != null)
654+
{
655+
_logger.PauseLogging();
656+
}
657+
#endif
658+
}
659+
660+
public static void ResumeLogger()
661+
{
662+
#if !DISABLE_BETAHUB_LOGGER
663+
if (_logger != null)
664+
{
665+
_logger.ResumeLogging();
666+
}
667+
#endif
668+
}
669+
648670
private void ValidateProviderCustomFields()
649671
{
650672
// Skip validation if we don't have the necessary credentials

Runtime/Scripts/Issue.cs

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public struct LogFileReference
4646
{
4747
public string path;
4848
public bool removeAfterUpload;
49+
public Logger logger; // Optional: if set, use logger.ReadLogFileBytes() instead of path
4950
}
5051

5152
public struct ScreenshotFileReference
@@ -214,19 +215,51 @@ public IEnumerator Publish(bool emailMyReport)
214215
// Helper method called after media upload completes
215216
private IEnumerator MarkMediaCompleteAndTryPublish()
216217
{
217-
lock (this)
218+
bool lockTaken = false;
219+
try
218220
{
219-
_mediaUploadComplete = true;
221+
System.Threading.Monitor.TryEnter(this, 1000, ref lockTaken); // 1 second timeout
222+
if (lockTaken)
223+
{
224+
_mediaUploadComplete = true;
225+
}
226+
else
227+
{
228+
Debug.LogWarning("Failed to acquire lock for MarkMediaCompleteAndTryPublish within timeout");
229+
}
230+
}
231+
finally
232+
{
233+
if (lockTaken)
234+
{
235+
System.Threading.Monitor.Exit(this);
236+
}
220237
}
221238
yield return CheckAndPublishIfReady();
222239
}
223240

224241
// Helper method called by the public Publish() method
225242
private IEnumerator RequestPublishAndTryPublish()
226243
{
227-
lock (this)
244+
bool lockTaken = false;
245+
try
228246
{
229-
_publishRequested = true;
247+
System.Threading.Monitor.TryEnter(this, 1000, ref lockTaken); // 1 second timeout
248+
if (lockTaken)
249+
{
250+
_publishRequested = true;
251+
}
252+
else
253+
{
254+
Debug.LogWarning("Failed to acquire lock for RequestPublishAndTryPublish within timeout");
255+
}
256+
}
257+
finally
258+
{
259+
if (lockTaken)
260+
{
261+
System.Threading.Monitor.Exit(this);
262+
}
230263
}
231264
yield return CheckAndPublishIfReady();
232265
}
@@ -235,13 +268,29 @@ private IEnumerator RequestPublishAndTryPublish()
235268
private IEnumerator CheckAndPublishIfReady()
236269
{
237270
bool shouldPublish = false;
238-
lock (this)
271+
bool lockTaken = false;
272+
try
273+
{
274+
System.Threading.Monitor.TryEnter(this, 1000, ref lockTaken); // 1 second timeout
275+
if (lockTaken)
276+
{
277+
// Check if publish is requested, media is done, not already published, and we have the necessary ID/token
278+
if (_publishRequested && _mediaUploadComplete && !_isPublished && !string.IsNullOrEmpty(Id) && !string.IsNullOrEmpty(_updateIssueAuthToken))
279+
{
280+
shouldPublish = true;
281+
_isPublished = true; // Prevent duplicate publish attempts
282+
}
283+
}
284+
else
285+
{
286+
Debug.LogWarning("Failed to acquire lock for CheckAndPublishIfReady within timeout");
287+
}
288+
}
289+
finally
239290
{
240-
// Check if publish is requested, media is done, not already published, and we have the necessary ID/token
241-
if (_publishRequested && _mediaUploadComplete && !_isPublished && !string.IsNullOrEmpty(Id) && !string.IsNullOrEmpty(_updateIssueAuthToken))
291+
if (lockTaken)
242292
{
243-
shouldPublish = true;
244-
_isPublished = true; // Prevent duplicate publish attempts
293+
System.Threading.Monitor.Exit(this);
245294
}
246295
}
247296

@@ -381,8 +430,25 @@ private IEnumerator PostScreenshot(ScreenshotFileReference screenshot)
381430

382431
private IEnumerator PostLogFile(LogFileReference logFile)
383432
{
384-
if (File.Exists(logFile.path))
433+
if (logFile.logger != null)
385434
{
435+
// Use logger's safe read method to avoid sharing violations
436+
Debug.Log("Reading BetaHub log file safely using Logger instance");
437+
byte[] fileData = logFile.logger.ReadLogFileBytes();
438+
439+
if (fileData != null)
440+
{
441+
string fileName = Path.GetFileName(logFile.logger.LogPath) ?? "BH_Player.log";
442+
yield return UploadStringAsFile("log_files", "log_file[file]", fileData, fileName, "text/plain");
443+
}
444+
else
445+
{
446+
Debug.LogError("Failed to read log file data from Logger instance");
447+
}
448+
}
449+
else if (File.Exists(logFile.path))
450+
{
451+
// Original file path logic
386452
yield return UploadFile("log_files", "log_file[file]", logFile.path, "text/plain");
387453

388454
if (logFile.removeAfterUpload)
@@ -409,9 +475,66 @@ private IEnumerator PostVideo(GameRecorder gameRecorder)
409475

410476
private IEnumerator UploadFile(string endpoint, string fieldName, string filePath, string contentType)
411477
{
478+
bool isLogFile = Path.GetExtension(filePath).Equals(".log", StringComparison.OrdinalIgnoreCase);
479+
byte[] fileData;
480+
481+
if (isLogFile)
482+
{
483+
if (BugReportUI.Logger != null && filePath == BugReportUI.Logger.LogPath)
484+
{
485+
Debug.Log("Reading log file safely using Logger instance");
486+
fileData = BugReportUI.Logger.ReadLogFileBytes();
487+
if (fileData == null)
488+
{
489+
Debug.LogError("Failed to read log file data from Logger instance");
490+
yield break;
491+
}
492+
}
493+
else
494+
{
495+
BugReportUI.PauseLogger();
496+
497+
try
498+
{
499+
fileData = File.ReadAllBytes(filePath);
500+
}
501+
catch (Exception ex)
502+
{
503+
Debug.LogError($"Error reading file {filePath}: {ex.Message}");
504+
BugReportUI.ResumeLogger();
505+
yield break;
506+
}
507+
508+
BugReportUI.ResumeLogger();
509+
}
510+
}
511+
else
512+
{
513+
// For non-log files, read normally
514+
try
515+
{
516+
fileData = File.ReadAllBytes(filePath);
517+
}
518+
catch (Exception ex)
519+
{
520+
Debug.LogError($"Error reading file {filePath}: {ex.Message}");
521+
yield break;
522+
}
523+
}
524+
525+
yield return UploadStringAsFile(endpoint, fieldName, fileData, Path.GetFileName(filePath), contentType);
526+
}
527+
528+
private IEnumerator UploadStringAsFile(string endpoint, string fieldName, byte[] fileData, string fileName, string contentType)
529+
{
530+
if (fileData == null)
531+
{
532+
Debug.LogError($"Cannot upload {fileName}: file data is null");
533+
yield break;
534+
}
535+
412536
WWWForm form = new WWWForm();
413-
byte[] fileData = File.ReadAllBytes(filePath);
414-
form.AddBinaryData(fieldName, fileData, Path.GetFileName(filePath), contentType);
537+
form.AddBinaryData(fieldName, fileData, fileName, contentType);
415538

416539
string url = $"{_betahubEndpoint}projects/{_projectId}/issues/g-{Id}/{endpoint}";
417540
using (UnityWebRequest www = UnityWebRequest.Post(url, form))
@@ -424,11 +547,11 @@ private IEnumerator UploadFile(string endpoint, string fieldName, string filePat
424547

425548
if (www.result != UnityWebRequest.Result.Success)
426549
{
427-
Debug.LogError($"Error uploading {Path.GetFileName(filePath)}: {www.error}");
550+
Debug.LogError($"Error uploading {fileName}: {www.error}");
428551
}
429552
else
430553
{
431-
Debug.Log($"{Path.GetFileName(filePath)} uploaded successfully!");
554+
Debug.Log($"{fileName} uploaded successfully!");
432555
}
433556
}
434557
}

0 commit comments

Comments
 (0)