com.elestrago.unity.generated-code-dump 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CHANGELOG.md.meta +7 -0
- package/Documentation~/api.md +583 -0
- package/Documentation~/examples/dump-in-batch-mode.md +43 -0
- package/Documentation~/examples/dump-now.md +29 -0
- package/Documentation~/examples/read-the-dump.md +63 -0
- package/Documentation~/examples/turn-on-the-dump.md +33 -0
- package/Documentation~/examples.md +24 -0
- package/Documentation~/manual.md +618 -0
- package/Editor/AssemblyInfo.cs +3 -0
- package/Editor/AssemblyInfo.cs.meta +2 -0
- package/Editor/GeneratedCodeDump.Editor.asmdef +16 -0
- package/Editor/GeneratedCodeDump.Editor.asmdef.meta +7 -0
- package/Editor/GeneratedCodeDumpArgs.cs +113 -0
- package/Editor/GeneratedCodeDumpArgs.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpFiles.cs +120 -0
- package/Editor/GeneratedCodeDumpFiles.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpFinisher.cs +103 -0
- package/Editor/GeneratedCodeDumpFinisher.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpIdentity.cs +40 -0
- package/Editor/GeneratedCodeDumpIdentity.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpJobs.cs +242 -0
- package/Editor/GeneratedCodeDumpJobs.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpMigration.cs +112 -0
- package/Editor/GeneratedCodeDumpMigration.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpNames.cs +104 -0
- package/Editor/GeneratedCodeDumpNames.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpOutput.cs +449 -0
- package/Editor/GeneratedCodeDumpOutput.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpPathBudget.cs +67 -0
- package/Editor/GeneratedCodeDumpPathBudget.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpPaths.cs +145 -0
- package/Editor/GeneratedCodeDumpPaths.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpProcess.cs +119 -0
- package/Editor/GeneratedCodeDumpProcess.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpRsp.cs +107 -0
- package/Editor/GeneratedCodeDumpRsp.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSession.cs +65 -0
- package/Editor/GeneratedCodeDumpSession.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettings.cs +111 -0
- package/Editor/GeneratedCodeDumpSettings.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsFile.cs +231 -0
- package/Editor/GeneratedCodeDumpSettingsFile.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsProvider.cs +93 -0
- package/Editor/GeneratedCodeDumpSettingsProvider.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsWatcher.cs +199 -0
- package/Editor/GeneratedCodeDumpSettingsWatcher.cs.meta +2 -0
- package/Editor/GeneratedCodeDumper.cs +689 -0
- package/Editor/GeneratedCodeDumper.cs.meta +2 -0
- package/Editor/GeneratedCodeMenuItems.cs +46 -0
- package/Editor/GeneratedCodeMenuItems.cs.meta +2 -0
- package/Editor.meta +8 -0
- package/LICENSE.md +21 -0
- package/LICENSE.md.meta +7 -0
- package/README.md +164 -0
- package/README.md.meta +7 -0
- package/package.json +31 -0
- package/package.json.meta +7 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Diagnostics;
|
|
4
|
+
using System.Globalization;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Linq;
|
|
7
|
+
using System.Text.RegularExpressions;
|
|
8
|
+
using System.Threading;
|
|
9
|
+
using UnityEditor;
|
|
10
|
+
using UnityEditor.Compilation;
|
|
11
|
+
using UnityEngine;
|
|
12
|
+
using Debug = UnityEngine.Debug;
|
|
13
|
+
|
|
14
|
+
namespace GeneratedCodeDump.Editor
|
|
15
|
+
{
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// After each compile, replays Unity's compiler response file of every rebuilt assembly that loads an
|
|
18
|
+
/// analyzer, with <c>-generatedfilesout</c>, so the files every source generator produced land in
|
|
19
|
+
/// <c>Library/GeneratedCodeDump/<Assembly>/<GeneratorAssembly>/</c>. The replay writes its
|
|
20
|
+
/// assemblies to the job's temp folder and never touches Unity's own Library/Bee outputs.
|
|
21
|
+
/// </summary>
|
|
22
|
+
/// <remarks>
|
|
23
|
+
/// <para>
|
|
24
|
+
/// A requested dump becomes a queued job in <see cref="GeneratedCodeDumpJobs"/>; at most
|
|
25
|
+
/// <see cref="MaxConcurrent"/> compilers run at once, and at most one job per assembly exists: a dump
|
|
26
|
+
/// asked for a queued assembly is already covered, one asked while its compiler runs replays it again
|
|
27
|
+
/// afterwards. The compiler runs as a background process with no redirected output (an unread pipe dies
|
|
28
|
+
/// with the domain and can take the compiler with it); diagnostics go to <c>.work/<Assembly>/errors.sarif</c>,
|
|
29
|
+
/// generated files and assemblies to the job's folder in <see cref="GeneratedCodeDumpPaths.ScratchRoot"/>.
|
|
30
|
+
/// <see cref="GeneratedCodeDumpFinisher"/> starts queued jobs and moves finished output into the dump layout
|
|
31
|
+
/// (<see cref="GeneratedCodeDumpOutput"/>).
|
|
32
|
+
/// </para>
|
|
33
|
+
/// <para>
|
|
34
|
+
/// Batch mode has no finisher thread: each request waits for its jobs, since the Editor can quit right
|
|
35
|
+
/// after. That includes the full dump the static constructor starts when the dump has just turned on, so a
|
|
36
|
+
/// CI or agent-launched Editor with the dump on blocks until the dump is complete.
|
|
37
|
+
/// </para>
|
|
38
|
+
/// <para>
|
|
39
|
+
/// Lock order, outermost first: <see cref="SessionLock"/>, <see cref="Gate"/>, the finisher's thread lock,
|
|
40
|
+
/// the job store's file lock. Every read-modify-write of the jobs holds <see cref="Gate"/>. Nothing here
|
|
41
|
+
/// calls Unity API off the main thread: the paths, the compiler and the batch-mode flag are read once, on
|
|
42
|
+
/// the main thread, by the static constructor. Any failure is a warning; nothing throws into Unity's
|
|
43
|
+
/// callbacks.
|
|
44
|
+
/// </para>
|
|
45
|
+
/// </remarks>
|
|
46
|
+
[InitializeOnLoad]
|
|
47
|
+
internal static class GeneratedCodeDumper
|
|
48
|
+
{
|
|
49
|
+
private const string StampFormat = "yyyy-MM-dd HH:mm:ss 'UTC'";
|
|
50
|
+
private const double JobTimeoutSeconds = 300;
|
|
51
|
+
private const int BatchPollMilliseconds = 50;
|
|
52
|
+
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// Compilers that may run at once: half the logical processors, between 1 and 8.
|
|
55
|
+
/// </summary>
|
|
56
|
+
internal static readonly int MaxConcurrent = Math.Max(1, Math.Min(8, Environment.ProcessorCount / 2));
|
|
57
|
+
|
|
58
|
+
private static readonly object SessionLock = new object();
|
|
59
|
+
private static readonly object Gate = new object();
|
|
60
|
+
private static readonly List<string> CompiledAssemblies = new List<string>();
|
|
61
|
+
private static readonly Regex SarifError = new Regex("\"level\"\\s*:\\s*\"error\"");
|
|
62
|
+
|
|
63
|
+
private static volatile bool _unavailable;
|
|
64
|
+
private static volatile bool _compiling;
|
|
65
|
+
private static bool _isBatchMode;
|
|
66
|
+
private static bool _batchGaveUp;
|
|
67
|
+
private static string _dotnet;
|
|
68
|
+
private static string _csc;
|
|
69
|
+
|
|
70
|
+
static GeneratedCodeDumper()
|
|
71
|
+
{
|
|
72
|
+
try
|
|
73
|
+
{
|
|
74
|
+
// An import worker is a separate Editor process; it must never start dumps of its own.
|
|
75
|
+
if (AssetDatabase.IsAssetImportWorkerProcess())
|
|
76
|
+
return;
|
|
77
|
+
|
|
78
|
+
CaptureMainThreadState();
|
|
79
|
+
|
|
80
|
+
// Before the setting is first read, so a 1.0.0 project keeps its switch.
|
|
81
|
+
GeneratedCodeDumpMigration.Run();
|
|
82
|
+
|
|
83
|
+
// Subscribed always and checked per compile, so switching the setting applies to the very next
|
|
84
|
+
// compile rather than after the one that reloads the domain.
|
|
85
|
+
CompilationPipeline.compilationStarted += OnCompilationStarted;
|
|
86
|
+
CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
|
|
87
|
+
CompilationPipeline.compilationFinished += OnCompilationFinished;
|
|
88
|
+
AssemblyReloadEvents.beforeAssemblyReload += Shutdown;
|
|
89
|
+
EditorApplication.quitting += Shutdown;
|
|
90
|
+
|
|
91
|
+
if (!_isBatchMode)
|
|
92
|
+
GeneratedCodeDumpSettingsWatcher.Start(DumpIfJustEnabled);
|
|
93
|
+
|
|
94
|
+
DumpIfJustEnabled();
|
|
95
|
+
|
|
96
|
+
// Jobs left by the previous domain, or by an Editor that quit mid-dump.
|
|
97
|
+
if (HasJobs())
|
|
98
|
+
Kick();
|
|
99
|
+
}
|
|
100
|
+
catch (Exception exception)
|
|
101
|
+
{
|
|
102
|
+
MarkUnavailable(exception.Message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// <summary>
|
|
107
|
+
/// Dumps everything when the dump has just turned on — Editor start, or the setting switched on in
|
|
108
|
+
/// Preferences, the menu or its file — or when its output folder is missing, so the folder is never
|
|
109
|
+
/// stale or absent while the dump is on. Turning it off drops the jobs that have not started. Safe on
|
|
110
|
+
/// any thread.
|
|
111
|
+
/// </summary>
|
|
112
|
+
internal static void DumpIfJustEnabled()
|
|
113
|
+
{
|
|
114
|
+
try
|
|
115
|
+
{
|
|
116
|
+
lock (SessionLock)
|
|
117
|
+
{
|
|
118
|
+
var marker = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.SessionMarkerFile);
|
|
119
|
+
var markerText = GeneratedCodeDumpFiles.ReadTextOrNull(marker);
|
|
120
|
+
|
|
121
|
+
var enabled = IsOn();
|
|
122
|
+
if (!enabled)
|
|
123
|
+
{
|
|
124
|
+
if (markerText != null)
|
|
125
|
+
{
|
|
126
|
+
GeneratedCodeDumpFiles.TryDeleteFile(marker);
|
|
127
|
+
DropQueued();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var markerIsThisProcess = markerText != null
|
|
134
|
+
&& GeneratedCodeDumpSession.IsMarkerOf(markerText, GeneratedCodeDumpProcess.CurrentId, GeneratedCodeDumpProcess.CurrentStartUtcTicks);
|
|
135
|
+
var rootExists = Directory.Exists(GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.Root));
|
|
136
|
+
|
|
137
|
+
if (!markerIsThisProcess)
|
|
138
|
+
{
|
|
139
|
+
CreateWorkFolder();
|
|
140
|
+
GeneratedCodeDumpFiles.WriteAtomic(
|
|
141
|
+
marker,
|
|
142
|
+
GeneratedCodeDumpSession.FormatMarker(GeneratedCodeDumpProcess.CurrentId, GeneratedCodeDumpProcess.CurrentStartUtcTicks)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (GeneratedCodeDumpSession.ShouldDumpAll(enabled, markerIsThisProcess, rootExists))
|
|
147
|
+
DumpAll();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (Exception exception)
|
|
151
|
+
{
|
|
152
|
+
MarkUnavailable(exception.Message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// <summary>
|
|
157
|
+
/// Queues every assembly of the newest build graph that loads an analyzer, whatever the setting.
|
|
158
|
+
/// Returns how many were queued. Safe on any thread.
|
|
159
|
+
/// </summary>
|
|
160
|
+
internal static int DumpAll()
|
|
161
|
+
{
|
|
162
|
+
try
|
|
163
|
+
{
|
|
164
|
+
if (!TryLocateCompiler(out var problem))
|
|
165
|
+
{
|
|
166
|
+
Debug.LogWarning(GeneratedCodeDumpIdentity.LogPrefix + problem);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Created even when nothing is queued, so a project without analyzers does not dump everything
|
|
171
|
+
// again on every reload.
|
|
172
|
+
Directory.CreateDirectory(GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.Root));
|
|
173
|
+
|
|
174
|
+
var beeArtifacts = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.BeeArtifactsFolder);
|
|
175
|
+
var queued = Enqueue(GeneratedCodeDumpRsp.ListAssemblies(beeArtifacts));
|
|
176
|
+
Debug.Log(
|
|
177
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}queued {queued} assemblies, at most {MaxConcurrent} compiling at once; "
|
|
178
|
+
+ $"output in {GeneratedCodeDumpPaths.Root}/"
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
Kick();
|
|
182
|
+
return queued;
|
|
183
|
+
}
|
|
184
|
+
catch (Exception exception)
|
|
185
|
+
{
|
|
186
|
+
Debug.LogWarning(GeneratedCodeDumpIdentity.LogPrefix + exception.Message);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// <summary>
|
|
192
|
+
/// Saves the setting and acts on it at once. Main thread (Preferences page and menu).
|
|
193
|
+
/// </summary>
|
|
194
|
+
internal static void SetEnabled(bool enabled)
|
|
195
|
+
{
|
|
196
|
+
try
|
|
197
|
+
{
|
|
198
|
+
GeneratedCodeDumpSettings.Current.Enabled = enabled;
|
|
199
|
+
}
|
|
200
|
+
catch (Exception exception)
|
|
201
|
+
{
|
|
202
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{GeneratedCodeDumpPaths.SettingsFile} could not be written: {exception.Message}");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
DumpIfJustEnabled();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// <summary>
|
|
210
|
+
/// One round of the finisher: moves the output of every exited replay into the dump, then starts queued
|
|
211
|
+
/// jobs while fewer than <see cref="MaxConcurrent"/> compilers run.
|
|
212
|
+
/// </summary>
|
|
213
|
+
internal static void Tick()
|
|
214
|
+
{
|
|
215
|
+
FinishExited();
|
|
216
|
+
StartQueued();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
internal static bool HasJobs()
|
|
220
|
+
{
|
|
221
|
+
return GeneratedCodeDumpJobs.Any(JobsPath);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// <summary>
|
|
225
|
+
/// How long batch mode waits for <paramref name="jobs"/> jobs: every job may take the full timeout, and
|
|
226
|
+
/// <paramref name="maxConcurrent"/> of them run at once.
|
|
227
|
+
/// </summary>
|
|
228
|
+
internal static double BatchWaitSeconds(int jobs, int maxConcurrent, double jobTimeoutSeconds)
|
|
229
|
+
{
|
|
230
|
+
var rounds = (Math.Max(jobs, 1) + Math.Max(maxConcurrent, 1) - 1) / Math.Max(maxConcurrent, 1);
|
|
231
|
+
return jobTimeoutSeconds * rounds;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private static string JobsPath => GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.JobsFile);
|
|
235
|
+
|
|
236
|
+
private static void CaptureMainThreadState()
|
|
237
|
+
{
|
|
238
|
+
_ = GeneratedCodeDumpPaths.ProjectRoot;
|
|
239
|
+
_ = GeneratedCodeDumpPaths.ScratchRoot;
|
|
240
|
+
_ = GeneratedCodeDumpSettings.Current;
|
|
241
|
+
_ = GeneratedCodeDumpProcess.CurrentId;
|
|
242
|
+
_isBatchMode = Application.isBatchMode;
|
|
243
|
+
|
|
244
|
+
var contents = EditorApplication.applicationContentsPath;
|
|
245
|
+
var dotnetName = Application.platform == RuntimePlatform.WindowsEditor ? "dotnet.exe" : "dotnet";
|
|
246
|
+
_dotnet = Path.Combine(contents, "NetCoreRuntime", dotnetName);
|
|
247
|
+
_csc = Path.Combine(contents, "DotNetSdkRoslyn", "csc.dll");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private static void OnCompilationStarted(object context)
|
|
251
|
+
{
|
|
252
|
+
_compiling = true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private static void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
|
|
256
|
+
{
|
|
257
|
+
try
|
|
258
|
+
{
|
|
259
|
+
if (!IsOn())
|
|
260
|
+
return;
|
|
261
|
+
|
|
262
|
+
if (messages != null && messages.Any(message => message.type == CompilerMessageType.Error))
|
|
263
|
+
return;
|
|
264
|
+
|
|
265
|
+
var assembly = Path.GetFileNameWithoutExtension(assemblyPath);
|
|
266
|
+
if (!string.IsNullOrEmpty(assembly) && !CompiledAssemblies.Contains(assembly))
|
|
267
|
+
CompiledAssemblies.Add(assembly);
|
|
268
|
+
}
|
|
269
|
+
catch (Exception exception)
|
|
270
|
+
{
|
|
271
|
+
MarkUnavailable(exception.Message);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static void OnCompilationFinished(object context)
|
|
276
|
+
{
|
|
277
|
+
_compiling = false;
|
|
278
|
+
if (CompiledAssemblies.Count == 0)
|
|
279
|
+
return;
|
|
280
|
+
|
|
281
|
+
var assemblies = CompiledAssemblies.ToArray();
|
|
282
|
+
CompiledAssemblies.Clear();
|
|
283
|
+
|
|
284
|
+
try
|
|
285
|
+
{
|
|
286
|
+
if (!IsOn())
|
|
287
|
+
return;
|
|
288
|
+
|
|
289
|
+
if (!TryLocateCompiler(out var problem))
|
|
290
|
+
{
|
|
291
|
+
MarkUnavailable(problem);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (Enqueue(assemblies) > 0)
|
|
296
|
+
Kick();
|
|
297
|
+
}
|
|
298
|
+
catch (Exception exception)
|
|
299
|
+
{
|
|
300
|
+
MarkUnavailable(exception.Message);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The finisher first: once it is stopping, a settings callback still running cannot start it again.
|
|
305
|
+
private static void Shutdown()
|
|
306
|
+
{
|
|
307
|
+
GeneratedCodeDumpFinisher.Stop();
|
|
308
|
+
GeneratedCodeDumpSettingsWatcher.Stop();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// <summary>
|
|
312
|
+
/// Adds a queued job for each of <paramref name="assemblies"/> whose response file loads an analyzer.
|
|
313
|
+
/// Returns how many of them now have a job.
|
|
314
|
+
/// </summary>
|
|
315
|
+
private static int Enqueue(IEnumerable<string> assemblies)
|
|
316
|
+
{
|
|
317
|
+
var beeArtifacts = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.BeeArtifactsFolder);
|
|
318
|
+
var wanted = new List<string>();
|
|
319
|
+
foreach (var assembly in assemblies.Distinct(StringComparer.Ordinal))
|
|
320
|
+
{
|
|
321
|
+
try
|
|
322
|
+
{
|
|
323
|
+
var rsp = GeneratedCodeDumpRsp.Find(beeArtifacts, assembly);
|
|
324
|
+
if (rsp != null && GeneratedCodeDumpArgs.HasAnalyzer(GeneratedCodeDumpRsp.ReadArguments(rsp)))
|
|
325
|
+
wanted.Add(assembly);
|
|
326
|
+
}
|
|
327
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
328
|
+
{
|
|
329
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{assembly}: {exception.Message}");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (wanted.Count == 0)
|
|
334
|
+
return 0;
|
|
335
|
+
|
|
336
|
+
// Created now, not when a replay finishes, so the reload that follows does not find the dump folder
|
|
337
|
+
// missing and dump everything again.
|
|
338
|
+
CreateWorkFolder();
|
|
339
|
+
|
|
340
|
+
lock (Gate)
|
|
341
|
+
{
|
|
342
|
+
var list = GeneratedCodeDumpJobs.Read(JobsPath);
|
|
343
|
+
|
|
344
|
+
// Left over by an Editor that quit or crashed mid-dump.
|
|
345
|
+
if (list.Jobs.Count == 0)
|
|
346
|
+
CleanScratch(true);
|
|
347
|
+
|
|
348
|
+
foreach (var assembly in wanted)
|
|
349
|
+
{
|
|
350
|
+
var job = list.Find(assembly);
|
|
351
|
+
if (job == null)
|
|
352
|
+
list.Add(assembly);
|
|
353
|
+
else if (job.State == GeneratedCodeDumpJobState.Running)
|
|
354
|
+
job.Rerun = true;
|
|
355
|
+
|
|
356
|
+
// A queued job reads the response file when it starts, so it covers this request too.
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
GeneratedCodeDumpJobs.Write(JobsPath, list);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return wanted.Count;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private static void DropQueued()
|
|
366
|
+
{
|
|
367
|
+
lock (Gate)
|
|
368
|
+
{
|
|
369
|
+
var list = GeneratedCodeDumpJobs.Read(JobsPath);
|
|
370
|
+
if (list.DropNotStarted())
|
|
371
|
+
GeneratedCodeDumpJobs.Write(JobsPath, list);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private static void Kick()
|
|
376
|
+
{
|
|
377
|
+
if (_isBatchMode)
|
|
378
|
+
FinishAllBlocking();
|
|
379
|
+
else
|
|
380
|
+
GeneratedCodeDumpFinisher.Start();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Once it has given up, it does not wait again in this domain: the leftover jobs are the next session's.
|
|
384
|
+
private static void FinishAllBlocking()
|
|
385
|
+
{
|
|
386
|
+
if (_batchGaveUp)
|
|
387
|
+
return;
|
|
388
|
+
|
|
389
|
+
try
|
|
390
|
+
{
|
|
391
|
+
var jobs = GeneratedCodeDumpJobs.Read(JobsPath).Jobs.Count;
|
|
392
|
+
var budget = BatchWaitSeconds(jobs, MaxConcurrent, JobTimeoutSeconds);
|
|
393
|
+
var watch = Stopwatch.StartNew();
|
|
394
|
+
while (HasJobs())
|
|
395
|
+
{
|
|
396
|
+
if (watch.Elapsed.TotalSeconds >= budget)
|
|
397
|
+
{
|
|
398
|
+
_batchGaveUp = true;
|
|
399
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}stopped waiting after {budget:0} s; the rest of the dump finishes in the next Editor session.");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
Tick();
|
|
404
|
+
if (HasJobs())
|
|
405
|
+
Thread.Sleep(BatchPollMilliseconds);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (Exception exception)
|
|
409
|
+
{
|
|
410
|
+
Debug.LogWarning(GeneratedCodeDumpIdentity.LogPrefix + exception.Message);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private static void StartQueued()
|
|
415
|
+
{
|
|
416
|
+
if (_compiling)
|
|
417
|
+
return;
|
|
418
|
+
|
|
419
|
+
lock (Gate)
|
|
420
|
+
{
|
|
421
|
+
var list = GeneratedCodeDumpJobs.Read(JobsPath);
|
|
422
|
+
var queued = list.Jobs.Where(job => job.State == GeneratedCodeDumpJobState.Queued).OrderBy(job => job.Id).ToList();
|
|
423
|
+
if (queued.Count == 0)
|
|
424
|
+
return;
|
|
425
|
+
|
|
426
|
+
if (!TryLocateCompiler(out var problem))
|
|
427
|
+
{
|
|
428
|
+
list.Jobs.RemoveAll(job => job.State == GeneratedCodeDumpJobState.Queued);
|
|
429
|
+
GeneratedCodeDumpJobs.Write(JobsPath, list);
|
|
430
|
+
Debug.LogWarning(GeneratedCodeDumpIdentity.LogPrefix + problem);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
var running = list.Jobs.Count(
|
|
435
|
+
job => job.State == GeneratedCodeDumpJobState.Running && GeneratedCodeDumpProcess.IsRunning(job.ProcessId, job.ProcessStartUtcTicks)
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
foreach (var job in queued)
|
|
439
|
+
{
|
|
440
|
+
if (running >= MaxConcurrent || GeneratedCodeDumpFinisher.IsStopping)
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
try
|
|
444
|
+
{
|
|
445
|
+
if (StartReplay(job))
|
|
446
|
+
running++;
|
|
447
|
+
else
|
|
448
|
+
list.Jobs.Remove(job);
|
|
449
|
+
}
|
|
450
|
+
catch (Exception exception)
|
|
451
|
+
{
|
|
452
|
+
list.Jobs.Remove(job);
|
|
453
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: {exception.Message}");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Recorded per job, so a reload right after never starts a second compiler for it.
|
|
457
|
+
GeneratedCodeDumpJobs.Write(JobsPath, list);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// False when the assembly has nothing to replay any more.
|
|
463
|
+
private static bool StartReplay(GeneratedCodeDumpJob job)
|
|
464
|
+
{
|
|
465
|
+
var beeArtifacts = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.BeeArtifactsFolder);
|
|
466
|
+
var rsp = GeneratedCodeDumpRsp.Find(beeArtifacts, job.Assembly);
|
|
467
|
+
if (rsp == null)
|
|
468
|
+
return false;
|
|
469
|
+
|
|
470
|
+
var lines = GeneratedCodeDumpRsp.ReadArguments(rsp);
|
|
471
|
+
if (!GeneratedCodeDumpArgs.HasAnalyzer(lines))
|
|
472
|
+
return false;
|
|
473
|
+
|
|
474
|
+
// One job per assembly at a time: two would share .work/<Assembly>/, and on Windows the first holds
|
|
475
|
+
// its files open.
|
|
476
|
+
var scratch = GeneratedCodeDumpPaths.ScratchDirectory(job.Id);
|
|
477
|
+
var workDir = GeneratedCodeDumpPaths.WorkDirectory(job.Assembly);
|
|
478
|
+
GeneratedCodeDumpFiles.DeleteDirectory(workDir);
|
|
479
|
+
GeneratedCodeDumpFiles.DeleteDirectory(scratch);
|
|
480
|
+
|
|
481
|
+
var generatedDir = GeneratedCodeDumpPaths.GeneratedFilesDirectory(job.Id);
|
|
482
|
+
var outDir = GeneratedCodeDumpPaths.ReplayOutputDirectory(job.Id);
|
|
483
|
+
CreateWorkFolder();
|
|
484
|
+
Directory.CreateDirectory(workDir);
|
|
485
|
+
Directory.CreateDirectory(generatedDir);
|
|
486
|
+
Directory.CreateDirectory(outDir);
|
|
487
|
+
|
|
488
|
+
// csc takes the assembly name from the -out file name, so it stays <Assembly>.dll.
|
|
489
|
+
var replay = Path.Combine(workDir, GeneratedCodeDumpPaths.ReplayFileName);
|
|
490
|
+
var arguments = GeneratedCodeDumpArgs.Rewrite(
|
|
491
|
+
lines,
|
|
492
|
+
Path.Combine(outDir, job.Assembly + ".dll"),
|
|
493
|
+
Path.Combine(outDir, job.Assembly + ".ref.dll"),
|
|
494
|
+
generatedDir,
|
|
495
|
+
Path.Combine(workDir, GeneratedCodeDumpPaths.ErrorLogFileName)
|
|
496
|
+
);
|
|
497
|
+
File.WriteAllLines(replay, arguments, GeneratedCodeDumpFiles.Utf8NoBom);
|
|
498
|
+
|
|
499
|
+
// Same switches as Unity's own compile action (Library/Bee/*.dag.json). /noconfig only works on
|
|
500
|
+
// the command line; inside a response file csc ignores it with CS2023.
|
|
501
|
+
var startInfo = new ProcessStartInfo
|
|
502
|
+
{
|
|
503
|
+
FileName = _dotnet,
|
|
504
|
+
Arguments = $"exec \"{_csc}\" /nostdlib /noconfig -nologo \"@{replay}\"",
|
|
505
|
+
WorkingDirectory = GeneratedCodeDumpPaths.ProjectRoot,
|
|
506
|
+
UseShellExecute = false,
|
|
507
|
+
CreateNoWindow = true
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
using (var process = Process.Start(startInfo))
|
|
511
|
+
{
|
|
512
|
+
if (process == null)
|
|
513
|
+
throw new InvalidOperationException($"{_dotnet} did not start.");
|
|
514
|
+
|
|
515
|
+
job.ProcessId = process.Id;
|
|
516
|
+
job.ProcessStartUtcTicks = GeneratedCodeDumpProcess.StartUtcTicks(process);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
job.StartedUtcTicks = DateTime.UtcNow.Ticks;
|
|
520
|
+
job.State = GeneratedCodeDumpJobState.Running;
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private static void FinishExited()
|
|
525
|
+
{
|
|
526
|
+
var snapshot = GeneratedCodeDumpJobs.Read(JobsPath);
|
|
527
|
+
var finished = false;
|
|
528
|
+
|
|
529
|
+
foreach (var job in snapshot.Jobs.Where(job => job.State == GeneratedCodeDumpJobState.Running))
|
|
530
|
+
{
|
|
531
|
+
if (GeneratedCodeDumpFinisher.IsStopping)
|
|
532
|
+
return;
|
|
533
|
+
|
|
534
|
+
try
|
|
535
|
+
{
|
|
536
|
+
if (GeneratedCodeDumpProcess.IsRunning(job.ProcessId, job.ProcessStartUtcTicks))
|
|
537
|
+
{
|
|
538
|
+
var seconds = (DateTime.UtcNow.Ticks - job.StartedUtcTicks) / (double)TimeSpan.TicksPerSecond;
|
|
539
|
+
if (seconds < JobTimeoutSeconds)
|
|
540
|
+
continue;
|
|
541
|
+
|
|
542
|
+
GeneratedCodeDumpProcess.TryKill(job.ProcessId, job.ProcessStartUtcTicks);
|
|
543
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: the compiler ran for over {JobTimeoutSeconds} s, so it was stopped and its output is ignored.");
|
|
544
|
+
}
|
|
545
|
+
else if (!job.Rerun)
|
|
546
|
+
{
|
|
547
|
+
SyncOutput(job);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (Exception exception)
|
|
551
|
+
{
|
|
552
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: {exception.Message}");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Last, so a reload before this point just repeats the finish.
|
|
556
|
+
Complete(job.Id);
|
|
557
|
+
GeneratedCodeDumpFiles.TryDeleteDirectory(GeneratedCodeDumpPaths.ScratchDirectory(job.Id));
|
|
558
|
+
finished = true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (finished)
|
|
562
|
+
CleanScratch(false);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// A job whose generated-files folder is gone counts as done: the previous dump is kept.
|
|
566
|
+
private static void SyncOutput(GeneratedCodeDumpJob job)
|
|
567
|
+
{
|
|
568
|
+
var generatedDir = GeneratedCodeDumpPaths.GeneratedFilesDirectory(job.Id);
|
|
569
|
+
if (!Directory.Exists(generatedDir))
|
|
570
|
+
return;
|
|
571
|
+
|
|
572
|
+
var workDir = GeneratedCodeDumpPaths.WorkDirectory(job.Assembly);
|
|
573
|
+
var errorLog = Path.Combine(workDir, GeneratedCodeDumpPaths.ErrorLogFileName);
|
|
574
|
+
var workFolder = $"{GeneratedCodeDumpPaths.WorkFolder}/{Path.GetFileName(workDir)}/";
|
|
575
|
+
if (!File.Exists(errorLog))
|
|
576
|
+
{
|
|
577
|
+
Debug.LogWarning(
|
|
578
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: the compiler stopped before writing {GeneratedCodeDumpPaths.ErrorLogFileName}, so the previous dump is kept. "
|
|
579
|
+
+ $"Its arguments: {workFolder}{GeneratedCodeDumpPaths.ReplayFileName}"
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (SarifError.IsMatch(File.ReadAllText(errorLog)))
|
|
585
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: the replay reported compile errors, so the dump can be incomplete. See {workFolder}{GeneratedCodeDumpPaths.ErrorLogFileName}");
|
|
586
|
+
|
|
587
|
+
var stamp = new DateTime(job.StartedUtcTicks, DateTimeKind.Utc).ToString(StampFormat, CultureInfo.InvariantCulture);
|
|
588
|
+
var budget = GeneratedCodeDumpPathBudget.Current;
|
|
589
|
+
var dumpRoot = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.Root);
|
|
590
|
+
var result = GeneratedCodeDumpOutput.Sync(generatedDir, dumpRoot, job.Assembly, stamp, budget);
|
|
591
|
+
if (result.TooLong.Count > 0)
|
|
592
|
+
{
|
|
593
|
+
Debug.LogWarning(
|
|
594
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: {result.TooLong.Count} files skipped: their path is over the {budget.File}-character limit of this OS; "
|
|
595
|
+
+ $"move the project to a shorter path.\n{string.Join("\n", result.TooLong)}"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (result.Failed.Count > 0)
|
|
600
|
+
{
|
|
601
|
+
Debug.LogWarning(
|
|
602
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}{job.Assembly}: {result.Failed.Count} files could not be replaced, most likely open in another program; "
|
|
603
|
+
+ $"they keep their older 'Dumped:' line and the next dump retries.\n{string.Join("\n", result.Failed)}"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Removes a finished job, or queues it again under a new id when a dump was asked meanwhile — also one
|
|
609
|
+
// asked after FinishExited took its snapshot, since this re-reads the jobs under the gate.
|
|
610
|
+
private static void Complete(long jobId)
|
|
611
|
+
{
|
|
612
|
+
try
|
|
613
|
+
{
|
|
614
|
+
lock (Gate)
|
|
615
|
+
{
|
|
616
|
+
var list = GeneratedCodeDumpJobs.Read(JobsPath);
|
|
617
|
+
var job = list.FindById(jobId);
|
|
618
|
+
if (job == null)
|
|
619
|
+
return;
|
|
620
|
+
|
|
621
|
+
list.Jobs.Remove(job);
|
|
622
|
+
if (job.Rerun)
|
|
623
|
+
list.Add(job.Assembly);
|
|
624
|
+
|
|
625
|
+
GeneratedCodeDumpJobs.Write(JobsPath, list);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
629
|
+
{
|
|
630
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{GeneratedCodeDumpPaths.JobsFile}: {exception.Message}");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private static void CleanScratch(bool all)
|
|
635
|
+
{
|
|
636
|
+
if (all)
|
|
637
|
+
GeneratedCodeDumpFiles.TryDeleteDirectory(GeneratedCodeDumpPaths.ScratchRoot);
|
|
638
|
+
else
|
|
639
|
+
GeneratedCodeDumpFiles.TryDeleteEmptyDirectory(GeneratedCodeDumpPaths.ScratchRoot);
|
|
640
|
+
|
|
641
|
+
GeneratedCodeDumpFiles.TryDeleteEmptyDirectory(GeneratedCodeDumpPaths.ScratchBase);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Hidden on Windows, so Open Folder shows only assembly folders. Creates the dump folder too.
|
|
645
|
+
private static void CreateWorkFolder()
|
|
646
|
+
{
|
|
647
|
+
GeneratedCodeDumpFiles.CreateHiddenDirectory(GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.WorkFolder));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private static bool TryLocateCompiler(out string problem)
|
|
651
|
+
{
|
|
652
|
+
if (!File.Exists(_dotnet))
|
|
653
|
+
{
|
|
654
|
+
problem = $"Unity's .NET runtime was not found at {_dotnet}.";
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!File.Exists(_csc))
|
|
659
|
+
{
|
|
660
|
+
problem = $"Unity's C# compiler was not found at {_csc}.";
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
var beeArtifacts = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.BeeArtifactsFolder);
|
|
665
|
+
if (!Directory.Exists(beeArtifacts))
|
|
666
|
+
{
|
|
667
|
+
problem = $"Unity's build folder {GeneratedCodeDumpPaths.BeeArtifactsFolder} was not found.";
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
problem = null;
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private static bool IsOn()
|
|
676
|
+
{
|
|
677
|
+
return !_unavailable && GeneratedCodeDumpSettings.IsEnabled;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private static void MarkUnavailable(string problem)
|
|
681
|
+
{
|
|
682
|
+
if (_unavailable)
|
|
683
|
+
return;
|
|
684
|
+
|
|
685
|
+
_unavailable = true;
|
|
686
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}{problem} The dump is off until the next domain reload.");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|