Skip to content

Commit bc18d3b

Browse files
authored
fix(rhino-importer): Use main thread always for document creation (#1161)
* Use main thread * format * configure await false * pass args only once
1 parent 7c7260c commit bc18d3b

File tree

4 files changed

+82
-45
lines changed

4 files changed

+82
-45
lines changed
Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics.CodeAnalysis;
1+
using System.Diagnostics.Contracts;
22
using Microsoft.Extensions.Logging;
33
using Rhino;
44
using Rhino.Runtime.InProcess;
@@ -9,55 +9,34 @@
99

1010
namespace Speckle.Importers.Rhino.Internal;
1111

12-
internal sealed class ImporterInstance(Sender sender, ILogger<ImporterInstance> logger) : IDisposable
12+
internal sealed class ImporterInstance(ImporterArgs args, Sender sender, ILogger<ImporterInstance> logger) : IDisposable
1313
{
14-
private readonly ILogger _logger = logger;
1514
private readonly RhinoCore _rhinoInstance = new(["/netcore-8"], WindowStyle.NoWindow);
1615

17-
private RhinoDoc? _rhinoDoc;
16+
private readonly RhinoDoc _rhinoDoc = OpenDocument(args, logger);
1817

19-
public async Task<ImporterResponse> Run(ImporterArgs args, CancellationToken cancellationToken)
20-
{
21-
using var scopeJobId = ActivityScope.SetTag("jobId", args.JobId);
22-
// using var scopeJobType = ActivityScope.SetTag("jobType", a.JobType);
23-
using var scopeAttempt = ActivityScope.SetTag("job.attempt", args.Attempt.ToString());
24-
using var scopeServerUrl = ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url);
25-
using var scopeProjectId = ActivityScope.SetTag("projectId", args.Project.id);
26-
using var scopeModelId = ActivityScope.SetTag("modelId", args.ModelId);
27-
using var scopeBlobId = ActivityScope.SetTag("blobId", args.BlobId);
28-
using var scopeFileType = ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.'));
29-
UserActivityScope.AddUserScope(args.Account);
30-
31-
var result = await TryImport(args, cancellationToken);
32-
return result;
33-
}
18+
private readonly IReadOnlyList<IDisposable> _scopes =
19+
[
20+
ActivityScope.SetTag("jobId", args.JobId),
21+
ActivityScope.SetTag("job.attempt", args.Attempt.ToString()),
22+
// ActivityScope.SetTag("jobType", args.JobType),
23+
ActivityScope.SetTag("serverUrl", args.Account.serverInfo.url),
24+
ActivityScope.SetTag("projectId", args.Project.id),
25+
ActivityScope.SetTag("modelId", args.ModelId),
26+
ActivityScope.SetTag("blobId", args.BlobId),
27+
ActivityScope.SetTag("fileType", Path.GetExtension(args.FilePath).TrimStart('.')),
28+
UserActivityScope.AddUserScope(args.Account),
29+
];
3430

35-
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
36-
private async Task<ImporterResponse> TryImport(ImporterArgs args, CancellationToken cancellationToken)
31+
public async Task<Version> RunRhinoImport(CancellationToken cancellationToken)
3732
{
3833
try
3934
{
40-
var version = await RunRhinoImport(args, cancellationToken);
41-
return new ImporterResponse { Version = version, ErrorMessage = null };
42-
}
43-
catch (Exception ex)
44-
{
45-
_logger.LogWarning(ex, "Import attempt failed with exception");
46-
return new ImporterResponse { ErrorMessage = ex.Message, Version = null };
47-
}
48-
}
49-
50-
private async Task<Version> RunRhinoImport(ImporterArgs args, CancellationToken cancellationToken)
51-
{
52-
try
53-
{
54-
using var config = GetConfig(Path.GetExtension(args.FilePath));
55-
logger.LogInformation("Opening file {FilePath}", args.FilePath);
56-
57-
_rhinoDoc = config.OpenInHeadlessDocument(args.FilePath);
5835
RhinoDoc.ActiveDoc = _rhinoDoc;
5936

60-
var version = await sender.Send(args.Project, args.ModelId, args.Account, cancellationToken);
37+
var version = await sender
38+
.Send(args.Project, args.ModelId, args.Account, cancellationToken)
39+
.ConfigureAwait(false);
6140
return version;
6241
}
6342
finally
@@ -66,6 +45,14 @@ private async Task<Version> RunRhinoImport(ImporterArgs args, CancellationToken
6645
}
6746
}
6847

48+
private static RhinoDoc OpenDocument(ImporterArgs args, ILogger logger)
49+
{
50+
using var config = GetConfig(Path.GetExtension(args.FilePath));
51+
logger.LogInformation("Opening file {FilePath}", args.FilePath);
52+
return config.OpenInHeadlessDocument(args.FilePath);
53+
}
54+
55+
[Pure]
6956
private static IFileTypeConfig GetConfig(string extension) =>
7057
extension.ToLowerInvariant() switch
7158
{
@@ -83,5 +70,9 @@ public void Dispose()
8370
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673
8471
_rhinoDoc?.Dispose();
8572
_rhinoInstance.Dispose();
73+
foreach (var scope in _scopes)
74+
{
75+
scope.Dispose();
76+
}
8677
}
8778
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace Speckle.Importers.Rhino.Internal;
4+
5+
internal sealed class ImporterInstanceFactory(Sender sender, ILogger<ImporterInstance> logger)
6+
{
7+
public ImporterInstance Create(ImporterArgs args) => new(args, sender, logger);
8+
}

Importers/Rhino/Speckle.Importers.Rhino/Internal/ServiceRegistration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static IServiceCollection AddRhinoImporter(this IServiceCollection servic
2020
services.AddTransient<Progress>();
2121
services.AddTransient<Sender>();
2222
services.AddTransient<ImporterInstance>();
23+
services.AddTransient<ImporterInstanceFactory>();
2324

2425
// override default thread context
2526
services.AddSingleton<IThreadContext>(new ImporterThreadContext());

Importers/Rhino/Speckle.Importers.Rhino/Program.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
using System.Text.Json;
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.Json;
23
using System.Text.Json.Serialization;
34
using Microsoft.Extensions.DependencyInjection;
45
using Microsoft.Extensions.Logging;
56
using RhinoInside;
67
using Speckle.Importers.Rhino.Internal;
8+
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
79

810
namespace Speckle.Importers.Rhino;
911

@@ -18,9 +20,12 @@ static Program()
1820
Console.WriteLine($"Loading Rhino @ {Resolver.RhinoSystemDirectory}");
1921
}
2022

23+
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "IPC")]
2124
public static async Task Main(string[] args)
2225
{
2326
ILogger? logger = null;
27+
ImporterInstance? importer = null;
28+
2429
try
2530
{
2631
var importerArgs = JsonSerializer.Deserialize<ImporterArgs>(args[0], s_serializerOptions);
@@ -32,13 +37,34 @@ public static async Task Main(string[] args)
3237
TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
3338
logger.LogCritical(eventArgs.Exception, "Unobserved Task Exception");
3439

35-
var importer = serviceProvider.GetRequiredService<ImporterInstance>();
40+
var factory = serviceProvider.GetRequiredService<ImporterInstanceFactory>();
41+
42+
// Error handling flow below here looks a bit of a mess, but we're having to navigate threading issues with rhino inside
43+
// https://discourse.mcneel.com/t/rhino-inside-fatal-app-crashes-when-disposing-headless-documents/208673/7
44+
try
45+
{
46+
// This needs to be called on the main thread
47+
importer = factory.Create(importerArgs);
48+
}
49+
catch (Exception ex)
50+
{
51+
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
52+
return;
53+
}
3654

55+
// As soon as the main thread is yielded, it will be hogged by Rhino
56+
// Task.Run ensures we run everything on a thread pool thread.
3757
await Task.Run(async () =>
3858
{
39-
var result = await importer.Run(importerArgs, CancellationToken.None);
40-
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
41-
File.WriteAllLines(importerArgs.ResultsPath, [serializedResult]);
59+
try
60+
{
61+
Version result = await importer.RunRhinoImport(CancellationToken.None).ConfigureAwait(false);
62+
WriteResult(new() { Version = result }, importerArgs.ResultsPath);
63+
}
64+
catch (Exception ex)
65+
{
66+
WriteResult(new() { ErrorMessage = ex.Message }, importerArgs.ResultsPath);
67+
}
4268
})
4369
.ConfigureAwait(false);
4470
}
@@ -53,7 +79,18 @@ await Task.Run(async () =>
5379
{
5480
Console.WriteLine(MESSAGE);
5581
}
82+
5683
throw;
5784
}
85+
finally
86+
{
87+
importer?.Dispose();
88+
}
89+
}
90+
91+
private static void WriteResult(ImporterResponse result, string resultsPath)
92+
{
93+
var serializedResult = JsonSerializer.Serialize(result, s_serializerOptions);
94+
File.WriteAllLines(resultsPath, [serializedResult]);
5895
}
5996
}

0 commit comments

Comments
 (0)