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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "GeneratedCodeDump.Editor",
|
|
3
|
+
"rootNamespace": "GeneratedCodeDump.Editor",
|
|
4
|
+
"references": [],
|
|
5
|
+
"includePlatforms": [
|
|
6
|
+
"Editor"
|
|
7
|
+
],
|
|
8
|
+
"excludePlatforms": [],
|
|
9
|
+
"allowUnsafeCode": false,
|
|
10
|
+
"overrideReferences": false,
|
|
11
|
+
"precompiledReferences": [],
|
|
12
|
+
"autoReferenced": true,
|
|
13
|
+
"defineConstraints": [],
|
|
14
|
+
"versionDefines": [],
|
|
15
|
+
"noEngineReferences": false
|
|
16
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
|
|
4
|
+
namespace GeneratedCodeDump.Editor
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Reads and rewrites a Unity compiler response file (one argument per line) so it can be replayed
|
|
8
|
+
/// with generated-files output, without overwriting any of Unity's own outputs. No UnityEditor use.
|
|
9
|
+
/// </summary>
|
|
10
|
+
internal static class GeneratedCodeDumpArgs
|
|
11
|
+
{
|
|
12
|
+
private static readonly string[] AnalyzerArgNames = { "analyzer" };
|
|
13
|
+
|
|
14
|
+
// Every argument that makes csc write a file: redirected so the replay never touches Library/Bee.
|
|
15
|
+
private static readonly string[] OutputArgNames =
|
|
16
|
+
{
|
|
17
|
+
"out",
|
|
18
|
+
"refout",
|
|
19
|
+
"doc",
|
|
20
|
+
"pdb",
|
|
21
|
+
"errorlog",
|
|
22
|
+
"generatedfilesout"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/// <summary>
|
|
26
|
+
/// True when the compilation loads at least one analyzer (<c>-analyzer:</c> or <c>/analyzer:</c>, any
|
|
27
|
+
/// case). Without one no source generator runs, so a replay could produce nothing.
|
|
28
|
+
/// </summary>
|
|
29
|
+
internal static bool HasAnalyzer(IEnumerable<string> lines)
|
|
30
|
+
{
|
|
31
|
+
foreach (var line in lines)
|
|
32
|
+
{
|
|
33
|
+
if (TryParse(line, out var name, out var value) && IsAny(name, AnalyzerArgNames) && value.Trim('"').Length > 0)
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// <summary>
|
|
41
|
+
/// Drops every output-writing argument (both '-' and '/' prefixes, any case) and appends
|
|
42
|
+
/// <c>-out</c>, <c>-refout</c>, <c>-generatedfilesout</c> and <c>-errorlog</c> with quoted paths.
|
|
43
|
+
/// Every other line is kept unchanged and in order.
|
|
44
|
+
/// </summary>
|
|
45
|
+
internal static List<string> Rewrite(
|
|
46
|
+
IEnumerable<string> lines,
|
|
47
|
+
string outDll,
|
|
48
|
+
string refDll,
|
|
49
|
+
string generatedDir,
|
|
50
|
+
string errorLog
|
|
51
|
+
)
|
|
52
|
+
{
|
|
53
|
+
var result = new List<string>();
|
|
54
|
+
foreach (var line in lines)
|
|
55
|
+
{
|
|
56
|
+
if (TryParse(line, out var name, out _) && IsAny(name, OutputArgNames))
|
|
57
|
+
continue;
|
|
58
|
+
|
|
59
|
+
result.Add(line);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result.Add("-out:" + Quote(outDll));
|
|
63
|
+
result.Add("-refout:" + Quote(refDll));
|
|
64
|
+
result.Add("-generatedfilesout:" + Quote(generatedDir));
|
|
65
|
+
result.Add("-errorlog:" + Quote(errorLog));
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Parses "-name:value" or "/name:value". The name must be letters only, so an absolute Unix source
|
|
70
|
+
// path such as "/home/me/a:b.cs" is not mistaken for an option.
|
|
71
|
+
private static bool TryParse(string line, out string name, out string value)
|
|
72
|
+
{
|
|
73
|
+
name = null;
|
|
74
|
+
value = null;
|
|
75
|
+
if (line == null)
|
|
76
|
+
return false;
|
|
77
|
+
|
|
78
|
+
var text = line.Trim();
|
|
79
|
+
if (text.Length < 3 || (text[0] != '-' && text[0] != '/'))
|
|
80
|
+
return false;
|
|
81
|
+
|
|
82
|
+
var colon = text.IndexOf(':');
|
|
83
|
+
if (colon < 2)
|
|
84
|
+
return false;
|
|
85
|
+
|
|
86
|
+
for (var i = 1; i < colon; i++)
|
|
87
|
+
{
|
|
88
|
+
if (!char.IsLetter(text[i]))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
name = text.Substring(1, colon - 1);
|
|
93
|
+
value = text.Substring(colon + 1).Trim();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static bool IsAny(string name, string[] candidates)
|
|
98
|
+
{
|
|
99
|
+
foreach (var candidate in candidates)
|
|
100
|
+
{
|
|
101
|
+
if (string.Equals(name, candidate, StringComparison.OrdinalIgnoreCase))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static string Quote(string path)
|
|
109
|
+
{
|
|
110
|
+
return "\"" + path + "\"";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
|
|
6
|
+
namespace GeneratedCodeDump.Editor
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// File helpers shared by the dump's state files (settings, jobs, session marker) and its work folders.
|
|
10
|
+
/// No UnityEditor use, so every thread can call them.
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal static class GeneratedCodeDumpFiles
|
|
13
|
+
{
|
|
14
|
+
internal static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
|
15
|
+
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// Writes <paramref name="text"/> to <c><path>.tmp</c>, then moves it over <paramref name="path"/>,
|
|
18
|
+
/// so a reader, or an Editor that stops mid-write, never sees half a file. Creates the folder.
|
|
19
|
+
/// </summary>
|
|
20
|
+
internal static void WriteAtomic(string path, string text)
|
|
21
|
+
{
|
|
22
|
+
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
23
|
+
var temp = path + ".tmp";
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
File.WriteAllText(temp, text, Utf8NoBom);
|
|
27
|
+
if (File.Exists(path))
|
|
28
|
+
File.Replace(temp, path, null);
|
|
29
|
+
else
|
|
30
|
+
File.Move(temp, path);
|
|
31
|
+
}
|
|
32
|
+
finally
|
|
33
|
+
{
|
|
34
|
+
TryDeleteFile(temp);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// <summary>
|
|
39
|
+
/// The file's text, or null when it does not exist. Any other failure throws, so a caller never takes
|
|
40
|
+
/// a file it could not read for an empty one.
|
|
41
|
+
/// </summary>
|
|
42
|
+
internal static string ReadTextOrNull(string path)
|
|
43
|
+
{
|
|
44
|
+
try
|
|
45
|
+
{
|
|
46
|
+
return File.ReadAllText(path);
|
|
47
|
+
}
|
|
48
|
+
catch (Exception exception) when (exception is FileNotFoundException || exception is DirectoryNotFoundException)
|
|
49
|
+
{
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// <summary>
|
|
55
|
+
/// Creates the folder and, on Windows, marks it Hidden, so Explorer lists only what sits next to it.
|
|
56
|
+
/// Failing to mark it is ignored.
|
|
57
|
+
/// </summary>
|
|
58
|
+
internal static void CreateHiddenDirectory(string path)
|
|
59
|
+
{
|
|
60
|
+
Directory.CreateDirectory(path);
|
|
61
|
+
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
|
|
62
|
+
return;
|
|
63
|
+
|
|
64
|
+
try
|
|
65
|
+
{
|
|
66
|
+
var attributes = File.GetAttributes(path);
|
|
67
|
+
if ((attributes & FileAttributes.Hidden) == 0)
|
|
68
|
+
File.SetAttributes(path, attributes | FileAttributes.Hidden);
|
|
69
|
+
}
|
|
70
|
+
catch (Exception)
|
|
71
|
+
{
|
|
72
|
+
// Only cosmetic.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
internal static void DeleteDirectory(string path)
|
|
77
|
+
{
|
|
78
|
+
if (Directory.Exists(path))
|
|
79
|
+
Directory.Delete(path, true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
internal static void TryDeleteDirectory(string path)
|
|
83
|
+
{
|
|
84
|
+
try
|
|
85
|
+
{
|
|
86
|
+
DeleteDirectory(path);
|
|
87
|
+
}
|
|
88
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
89
|
+
{
|
|
90
|
+
// Held open by another process; the next dump retries.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
internal static void TryDeleteEmptyDirectory(string path)
|
|
95
|
+
{
|
|
96
|
+
try
|
|
97
|
+
{
|
|
98
|
+
if (Directory.Exists(path) && !Directory.EnumerateFileSystemEntries(path).Any())
|
|
99
|
+
Directory.Delete(path);
|
|
100
|
+
}
|
|
101
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
102
|
+
{
|
|
103
|
+
// Not empty any more, or held open; the next dump retries.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
internal static void TryDeleteFile(string path)
|
|
108
|
+
{
|
|
109
|
+
try
|
|
110
|
+
{
|
|
111
|
+
if (File.Exists(path))
|
|
112
|
+
File.Delete(path);
|
|
113
|
+
}
|
|
114
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
115
|
+
{
|
|
116
|
+
// Held open by another process; harmless.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Threading;
|
|
3
|
+
using UnityEngine;
|
|
4
|
+
|
|
5
|
+
namespace GeneratedCodeDump.Editor
|
|
6
|
+
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Background thread that runs <see cref="GeneratedCodeDumper.Tick"/> every 250 ms while there are jobs:
|
|
9
|
+
/// it starts queued replays and moves the output of finished ones into the dump. Unlike
|
|
10
|
+
/// <c>EditorApplication.update</c>, it keeps going while the Editor sits in the background. Not used in
|
|
11
|
+
/// batch mode, which finishes the jobs on the calling thread.
|
|
12
|
+
/// </summary>
|
|
13
|
+
/// <remarks>
|
|
14
|
+
/// Stopped before each domain reload and on quit, waiting up to 2 s for the current tick. A tick cut short
|
|
15
|
+
/// by the reload is repeated by the next domain: a job is removed only after its output is in place.
|
|
16
|
+
/// </remarks>
|
|
17
|
+
internal static class GeneratedCodeDumpFinisher
|
|
18
|
+
{
|
|
19
|
+
private const int TickMilliseconds = 250;
|
|
20
|
+
private const int JoinMilliseconds = 2000;
|
|
21
|
+
|
|
22
|
+
private static readonly object ThreadLock = new object();
|
|
23
|
+
private static readonly ManualResetEvent StopSignal = new ManualResetEvent(false);
|
|
24
|
+
|
|
25
|
+
private static Thread _thread;
|
|
26
|
+
private static volatile bool _stopping;
|
|
27
|
+
private static bool _warned;
|
|
28
|
+
|
|
29
|
+
internal static bool IsStopping => _stopping;
|
|
30
|
+
|
|
31
|
+
/// <summary>
|
|
32
|
+
/// Starts the thread unless it runs already. Call it after adding jobs.
|
|
33
|
+
/// </summary>
|
|
34
|
+
internal static void Start()
|
|
35
|
+
{
|
|
36
|
+
lock (ThreadLock)
|
|
37
|
+
{
|
|
38
|
+
if (_thread != null || _stopping)
|
|
39
|
+
return;
|
|
40
|
+
|
|
41
|
+
_thread = new Thread(Loop) { IsBackground = true, Name = GeneratedCodeDumpIdentity.ThreadName };
|
|
42
|
+
_thread.Start();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
internal static void Stop()
|
|
47
|
+
{
|
|
48
|
+
Thread thread;
|
|
49
|
+
lock (ThreadLock)
|
|
50
|
+
{
|
|
51
|
+
_stopping = true;
|
|
52
|
+
thread = _thread;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
StopSignal.Set();
|
|
56
|
+
thread?.Join(JoinMilliseconds);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static void Loop()
|
|
60
|
+
{
|
|
61
|
+
try
|
|
62
|
+
{
|
|
63
|
+
while (!_stopping)
|
|
64
|
+
{
|
|
65
|
+
GeneratedCodeDumper.Tick();
|
|
66
|
+
|
|
67
|
+
// Decided under the lock that Start takes, so a job added meanwhile either sees this thread
|
|
68
|
+
// still running or starts a new one.
|
|
69
|
+
lock (ThreadLock)
|
|
70
|
+
{
|
|
71
|
+
if (_stopping || !GeneratedCodeDumper.HasJobs())
|
|
72
|
+
{
|
|
73
|
+
_thread = null;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
StopSignal.WaitOne(TickMilliseconds);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (ThreadAbortException)
|
|
82
|
+
{
|
|
83
|
+
// The domain is unloading; the next one picks the jobs up.
|
|
84
|
+
}
|
|
85
|
+
catch (Exception exception)
|
|
86
|
+
{
|
|
87
|
+
if (!_warned)
|
|
88
|
+
{
|
|
89
|
+
_warned = true;
|
|
90
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}the background dump stopped: {exception.Message} It restarts with the next dump.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally
|
|
94
|
+
{
|
|
95
|
+
lock (ThreadLock)
|
|
96
|
+
{
|
|
97
|
+
if (_thread == Thread.CurrentThread)
|
|
98
|
+
_thread = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
namespace GeneratedCodeDump.Editor
|
|
2
|
+
{
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Every name by which the generated-code dump shows itself: its Library/ folder, log prefix, thread,
|
|
5
|
+
/// temp folder, menu and Preferences page.
|
|
6
|
+
/// </summary>
|
|
7
|
+
internal static class GeneratedCodeDumpIdentity
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// The package name. A constant because <c>PackageInfo.FindForAssembly</c> returns null when the package
|
|
11
|
+
/// sits under Assets/. Up to 1.0.0 it was also the folder name under Library/
|
|
12
|
+
/// (<see cref="GeneratedCodeDumpPaths.LegacyRoot"/>).
|
|
13
|
+
/// </summary>
|
|
14
|
+
internal const string PackageName = "com.elestrago.unity.generated-code-dump";
|
|
15
|
+
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// The folder under Library/ that holds the dump and its <c>.work/</c> folder, kept short for Windows'
|
|
18
|
+
/// 260-character path limit.
|
|
19
|
+
/// </summary>
|
|
20
|
+
internal const string LibraryFolderName = "GeneratedCodeDump";
|
|
21
|
+
|
|
22
|
+
internal const string DisplayName = "Generated Code Dump";
|
|
23
|
+
internal const string LogPrefix = "[" + DisplayName + "] ";
|
|
24
|
+
internal const string ThreadName = "Generated code dump";
|
|
25
|
+
|
|
26
|
+
/// <summary>
|
|
27
|
+
/// Folder under the system temp folder that holds the compiler outputs, kept short for Windows'
|
|
28
|
+
/// 260-character path limit.
|
|
29
|
+
/// </summary>
|
|
30
|
+
internal const string ScratchFolderName = "GenDump";
|
|
31
|
+
|
|
32
|
+
/// <summary>
|
|
33
|
+
/// The temp folder's name up to 1.0.0. The migration deletes this project's folder in it.
|
|
34
|
+
/// </summary>
|
|
35
|
+
internal const string LegacyScratchFolderName = "GCDump";
|
|
36
|
+
|
|
37
|
+
internal const string MenuRoot = "Tools/" + DisplayName + "/";
|
|
38
|
+
internal const string SettingsPath = "Preferences/" + DisplayName;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Globalization;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using System.Text;
|
|
6
|
+
|
|
7
|
+
namespace GeneratedCodeDump.Editor
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Where a dump job stands: waiting for a free compiler slot, or its compiler started.
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal enum GeneratedCodeDumpJobState
|
|
13
|
+
{
|
|
14
|
+
Queued,
|
|
15
|
+
Running
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// <summary>
|
|
19
|
+
/// One requested replay of an assembly whose output is not in the dump folder yet.
|
|
20
|
+
/// </summary>
|
|
21
|
+
internal sealed class GeneratedCodeDumpJob
|
|
22
|
+
{
|
|
23
|
+
internal long Id;
|
|
24
|
+
internal string Assembly;
|
|
25
|
+
internal GeneratedCodeDumpJobState State;
|
|
26
|
+
internal int ProcessId;
|
|
27
|
+
internal long ProcessStartUtcTicks;
|
|
28
|
+
|
|
29
|
+
/// <summary>
|
|
30
|
+
/// UTC wall time when the compiler was started: the job's timeout counts from it, and it is the
|
|
31
|
+
/// <c>Dumped:</c> time of the files.
|
|
32
|
+
/// </summary>
|
|
33
|
+
internal long StartedUtcTicks;
|
|
34
|
+
|
|
35
|
+
/// <summary>
|
|
36
|
+
/// A dump of the assembly was asked while its compiler ran: replay again once it is done.
|
|
37
|
+
/// </summary>
|
|
38
|
+
internal bool Rerun;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// <summary>
|
|
42
|
+
/// The content of the job store: the jobs, at most one per assembly, and the next id to hand out.
|
|
43
|
+
/// </summary>
|
|
44
|
+
internal sealed class GeneratedCodeDumpJobList
|
|
45
|
+
{
|
|
46
|
+
internal readonly List<GeneratedCodeDumpJob> Jobs = new List<GeneratedCodeDumpJob>();
|
|
47
|
+
internal long NextId = 1;
|
|
48
|
+
|
|
49
|
+
internal GeneratedCodeDumpJob Find(string assembly)
|
|
50
|
+
{
|
|
51
|
+
return Jobs.Find(job => string.Equals(job.Assembly, assembly, StringComparison.Ordinal));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
internal GeneratedCodeDumpJob FindById(long id)
|
|
55
|
+
{
|
|
56
|
+
return Jobs.Find(job => job.Id == id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// <summary>
|
|
60
|
+
/// Adds a queued job for <paramref name="assembly"/> with a fresh id.
|
|
61
|
+
/// </summary>
|
|
62
|
+
internal GeneratedCodeDumpJob Add(string assembly)
|
|
63
|
+
{
|
|
64
|
+
var job = new GeneratedCodeDumpJob { Id = NextId++, Assembly = assembly, State = GeneratedCodeDumpJobState.Queued };
|
|
65
|
+
Jobs.Add(job);
|
|
66
|
+
return job;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// <summary>
|
|
70
|
+
/// Drops every replay that has not started: the queued jobs, and the reruns asked of running ones.
|
|
71
|
+
/// Returns whether anything changed.
|
|
72
|
+
/// </summary>
|
|
73
|
+
internal bool DropNotStarted()
|
|
74
|
+
{
|
|
75
|
+
var changed = Jobs.RemoveAll(job => job.State == GeneratedCodeDumpJobState.Queued) > 0;
|
|
76
|
+
foreach (var job in Jobs.Where(job => job.Rerun))
|
|
77
|
+
{
|
|
78
|
+
job.Rerun = false;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return changed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>
|
|
87
|
+
/// The dump jobs, stored in <see cref="GeneratedCodeDumpPaths.JobsFile"/> so they outlive the domain
|
|
88
|
+
/// reload that follows every compile, and an Editor restart. Ids only ever go up, so a new job never
|
|
89
|
+
/// reuses the scratch folder of an old one.
|
|
90
|
+
/// </summary>
|
|
91
|
+
/// <remarks>
|
|
92
|
+
/// Format: a <c>next <id></c> line, then one tab-separated line per job:
|
|
93
|
+
/// <c>job, id, queued|running, rerun 0|1, process id, process start ticks, started ticks, assembly</c>.
|
|
94
|
+
/// <see cref="Read"/> and <see cref="Write"/> hold a lock of their own; a read-modify-write also needs
|
|
95
|
+
/// the dumper's gate, which callers take outside of it.
|
|
96
|
+
/// </remarks>
|
|
97
|
+
internal static class GeneratedCodeDumpJobs
|
|
98
|
+
{
|
|
99
|
+
private const string NextTag = "next";
|
|
100
|
+
private const string JobTag = "job";
|
|
101
|
+
private const string QueuedName = "queued";
|
|
102
|
+
private const string RunningName = "running";
|
|
103
|
+
private const int JobFieldCount = 8;
|
|
104
|
+
|
|
105
|
+
private static readonly object FileLock = new object();
|
|
106
|
+
|
|
107
|
+
/// <summary>
|
|
108
|
+
/// The jobs in <paramref name="path"/>; none when the file does not exist. Throws when it exists but
|
|
109
|
+
/// cannot be read, so a caller never overwrites jobs it did not see.
|
|
110
|
+
/// </summary>
|
|
111
|
+
internal static GeneratedCodeDumpJobList Read(string path)
|
|
112
|
+
{
|
|
113
|
+
lock (FileLock)
|
|
114
|
+
{
|
|
115
|
+
var text = GeneratedCodeDumpFiles.ReadTextOrNull(path);
|
|
116
|
+
return text == null ? new GeneratedCodeDumpJobList() : Parse(text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
internal static void Write(string path, GeneratedCodeDumpJobList list)
|
|
121
|
+
{
|
|
122
|
+
lock (FileLock)
|
|
123
|
+
GeneratedCodeDumpFiles.WriteAtomic(path, Format(list));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
internal static bool Any(string path)
|
|
127
|
+
{
|
|
128
|
+
return Read(path).Jobs.Count > 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
internal static string Format(GeneratedCodeDumpJobList list)
|
|
132
|
+
{
|
|
133
|
+
var builder = new StringBuilder();
|
|
134
|
+
builder.Append(NextTag).Append(' ').Append(ValidNextId(list).ToString(CultureInfo.InvariantCulture)).Append('\n');
|
|
135
|
+
|
|
136
|
+
foreach (var job in list.Jobs.OrderBy(job => job.Id))
|
|
137
|
+
{
|
|
138
|
+
builder.Append(JobTag)
|
|
139
|
+
.Append('\t').Append(job.Id.ToString(CultureInfo.InvariantCulture))
|
|
140
|
+
.Append('\t').Append(job.State == GeneratedCodeDumpJobState.Running ? RunningName : QueuedName)
|
|
141
|
+
.Append('\t').Append(job.Rerun ? '1' : '0')
|
|
142
|
+
.Append('\t').Append(job.ProcessId.ToString(CultureInfo.InvariantCulture))
|
|
143
|
+
.Append('\t').Append(job.ProcessStartUtcTicks.ToString(CultureInfo.InvariantCulture))
|
|
144
|
+
.Append('\t').Append(job.StartedUtcTicks.ToString(CultureInfo.InvariantCulture))
|
|
145
|
+
.Append('\t').Append(job.Assembly)
|
|
146
|
+
.Append('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return builder.ToString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// <summary>
|
|
153
|
+
/// Parses <see cref="Format"/>'s output. A malformed line, or a second job with an id or assembly
|
|
154
|
+
/// already seen, is skipped; <see cref="GeneratedCodeDumpJobList.NextId"/> always ends above the
|
|
155
|
+
/// highest job id.
|
|
156
|
+
/// </summary>
|
|
157
|
+
internal static GeneratedCodeDumpJobList Parse(string text)
|
|
158
|
+
{
|
|
159
|
+
var list = new GeneratedCodeDumpJobList();
|
|
160
|
+
var next = 1L;
|
|
161
|
+
|
|
162
|
+
foreach (var rawLine in text.Split('\n'))
|
|
163
|
+
{
|
|
164
|
+
var line = rawLine.TrimEnd('\r');
|
|
165
|
+
if (line.Length == 0)
|
|
166
|
+
continue;
|
|
167
|
+
|
|
168
|
+
if (line.StartsWith(NextTag + " ", StringComparison.Ordinal))
|
|
169
|
+
{
|
|
170
|
+
if (TryParseLong(line.Substring(NextTag.Length + 1), out var value) && value > next)
|
|
171
|
+
next = value;
|
|
172
|
+
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (TryParseJob(line, out var job) && list.FindById(job.Id) == null && list.Find(job.Assembly) == null)
|
|
177
|
+
list.Jobs.Add(job);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
list.NextId = next;
|
|
181
|
+
list.NextId = ValidNextId(list);
|
|
182
|
+
return list;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static long ValidNextId(GeneratedCodeDumpJobList list)
|
|
186
|
+
{
|
|
187
|
+
var next = Math.Max(1, list.NextId);
|
|
188
|
+
foreach (var job in list.Jobs)
|
|
189
|
+
next = Math.Max(next, job.Id + 1);
|
|
190
|
+
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private static bool TryParseJob(string line, out GeneratedCodeDumpJob job)
|
|
195
|
+
{
|
|
196
|
+
job = null;
|
|
197
|
+
var fields = line.Split('\t');
|
|
198
|
+
if (fields.Length != JobFieldCount || fields[0] != JobTag)
|
|
199
|
+
return false;
|
|
200
|
+
|
|
201
|
+
GeneratedCodeDumpJobState state;
|
|
202
|
+
if (fields[2] == QueuedName)
|
|
203
|
+
state = GeneratedCodeDumpJobState.Queued;
|
|
204
|
+
else if (fields[2] == RunningName)
|
|
205
|
+
state = GeneratedCodeDumpJobState.Running;
|
|
206
|
+
else
|
|
207
|
+
return false;
|
|
208
|
+
|
|
209
|
+
if (fields[3] != "0" && fields[3] != "1")
|
|
210
|
+
return false;
|
|
211
|
+
|
|
212
|
+
if (!TryParseLong(fields[1], out var id) || id < 1
|
|
213
|
+
|| !int.TryParse(fields[4], NumberStyles.None, CultureInfo.InvariantCulture, out var processId)
|
|
214
|
+
|| !TryParseLong(fields[5], out var processStart)
|
|
215
|
+
|| !TryParseLong(fields[6], out var started))
|
|
216
|
+
{
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var assembly = fields[7];
|
|
221
|
+
if (assembly.Length == 0 || assembly.Trim().Length != assembly.Length)
|
|
222
|
+
return false;
|
|
223
|
+
|
|
224
|
+
job = new GeneratedCodeDumpJob
|
|
225
|
+
{
|
|
226
|
+
Id = id,
|
|
227
|
+
Assembly = assembly,
|
|
228
|
+
State = state,
|
|
229
|
+
Rerun = fields[3] == "1",
|
|
230
|
+
ProcessId = processId,
|
|
231
|
+
ProcessStartUtcTicks = processStart,
|
|
232
|
+
StartedUtcTicks = started
|
|
233
|
+
};
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private static bool TryParseLong(string text, out long value)
|
|
238
|
+
{
|
|
239
|
+
return long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|