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,231 @@
|
|
|
1
|
+
using System.Globalization;
|
|
2
|
+
using System.Text;
|
|
3
|
+
|
|
4
|
+
namespace GeneratedCodeDump.Editor
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// The text of the settings file, <c>{"enabled": true}</c>: written and parsed without
|
|
8
|
+
/// <c>JsonUtility</c>, so the settings watcher can read it off the main thread. No UnityEditor use.
|
|
9
|
+
/// </summary>
|
|
10
|
+
/// <remarks>
|
|
11
|
+
/// The parser accepts one flat JSON object: string keys, and string, number, true, false or null values.
|
|
12
|
+
/// <c>enabled</c> must be exactly <c>true</c> or <c>false</c>; a missing <c>enabled</c> means off.
|
|
13
|
+
/// </remarks>
|
|
14
|
+
internal static class GeneratedCodeDumpSettingsFile
|
|
15
|
+
{
|
|
16
|
+
internal const string EnabledKey = "enabled";
|
|
17
|
+
|
|
18
|
+
internal static string Format(bool enabled)
|
|
19
|
+
{
|
|
20
|
+
return "{\n \"" + EnabledKey + "\": " + (enabled ? "true" : "false") + "\n}\n";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// <summary>
|
|
24
|
+
/// False when <paramref name="json"/> is not a flat JSON object with a boolean <c>enabled</c> (or none).
|
|
25
|
+
/// </summary>
|
|
26
|
+
internal static bool TryParse(string json, out bool enabled)
|
|
27
|
+
{
|
|
28
|
+
if (Parse(json, out enabled))
|
|
29
|
+
return true;
|
|
30
|
+
|
|
31
|
+
enabled = false;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static bool Parse(string json, out bool enabled)
|
|
36
|
+
{
|
|
37
|
+
enabled = false;
|
|
38
|
+
if (json == null)
|
|
39
|
+
return false;
|
|
40
|
+
|
|
41
|
+
var index = 0;
|
|
42
|
+
if (json.Length > 0 && json[0] == '\uFEFF')
|
|
43
|
+
index++;
|
|
44
|
+
|
|
45
|
+
SkipWhitespace(json, ref index);
|
|
46
|
+
if (!Consume(json, ref index, '{'))
|
|
47
|
+
return false;
|
|
48
|
+
|
|
49
|
+
SkipWhitespace(json, ref index);
|
|
50
|
+
if (Consume(json, ref index, '}'))
|
|
51
|
+
return AtEnd(json, index);
|
|
52
|
+
|
|
53
|
+
while (true)
|
|
54
|
+
{
|
|
55
|
+
if (!TryReadString(json, ref index, out var key))
|
|
56
|
+
return false;
|
|
57
|
+
|
|
58
|
+
SkipWhitespace(json, ref index);
|
|
59
|
+
if (!Consume(json, ref index, ':'))
|
|
60
|
+
return false;
|
|
61
|
+
|
|
62
|
+
SkipWhitespace(json, ref index);
|
|
63
|
+
if (key == EnabledKey)
|
|
64
|
+
{
|
|
65
|
+
if (ConsumeWord(json, ref index, "true"))
|
|
66
|
+
enabled = true;
|
|
67
|
+
else if (ConsumeWord(json, ref index, "false"))
|
|
68
|
+
enabled = false;
|
|
69
|
+
else
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
else if (!TrySkipScalar(json, ref index))
|
|
73
|
+
{
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
SkipWhitespace(json, ref index);
|
|
78
|
+
if (Consume(json, ref index, ','))
|
|
79
|
+
{
|
|
80
|
+
SkipWhitespace(json, ref index);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Consume(json, ref index, '}') && AtEnd(json, index);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static bool AtEnd(string json, int index)
|
|
89
|
+
{
|
|
90
|
+
SkipWhitespace(json, ref index);
|
|
91
|
+
return index == json.Length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static void SkipWhitespace(string json, ref int index)
|
|
95
|
+
{
|
|
96
|
+
while (index < json.Length && (json[index] == ' ' || json[index] == '\t' || json[index] == '\r' || json[index] == '\n'))
|
|
97
|
+
index++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static bool Consume(string json, ref int index, char expected)
|
|
101
|
+
{
|
|
102
|
+
if (index >= json.Length || json[index] != expected)
|
|
103
|
+
return false;
|
|
104
|
+
|
|
105
|
+
index++;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static bool ConsumeWord(string json, ref int index, string word)
|
|
110
|
+
{
|
|
111
|
+
if (string.CompareOrdinal(json, index, word, 0, word.Length) != 0)
|
|
112
|
+
return false;
|
|
113
|
+
|
|
114
|
+
index += word.Length;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static bool TrySkipScalar(string json, ref int index)
|
|
119
|
+
{
|
|
120
|
+
if (index >= json.Length)
|
|
121
|
+
return false;
|
|
122
|
+
|
|
123
|
+
if (json[index] == '"')
|
|
124
|
+
return TryReadString(json, ref index, out _);
|
|
125
|
+
|
|
126
|
+
if (ConsumeWord(json, ref index, "true") || ConsumeWord(json, ref index, "false") || ConsumeWord(json, ref index, "null"))
|
|
127
|
+
return true;
|
|
128
|
+
|
|
129
|
+
return TrySkipNumber(json, ref index);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static bool TrySkipNumber(string json, ref int index)
|
|
133
|
+
{
|
|
134
|
+
Consume(json, ref index, '-');
|
|
135
|
+
if (!SkipDigits(json, ref index))
|
|
136
|
+
return false;
|
|
137
|
+
|
|
138
|
+
if (Consume(json, ref index, '.') && !SkipDigits(json, ref index))
|
|
139
|
+
return false;
|
|
140
|
+
|
|
141
|
+
if (Consume(json, ref index, 'e') || Consume(json, ref index, 'E'))
|
|
142
|
+
{
|
|
143
|
+
if (!Consume(json, ref index, '+'))
|
|
144
|
+
Consume(json, ref index, '-');
|
|
145
|
+
|
|
146
|
+
if (!SkipDigits(json, ref index))
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static bool SkipDigits(string json, ref int index)
|
|
154
|
+
{
|
|
155
|
+
var start = index;
|
|
156
|
+
while (index < json.Length && json[index] >= '0' && json[index] <= '9')
|
|
157
|
+
index++;
|
|
158
|
+
|
|
159
|
+
return index > start;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static bool TryReadString(string json, ref int index, out string value)
|
|
163
|
+
{
|
|
164
|
+
value = null;
|
|
165
|
+
if (!Consume(json, ref index, '"'))
|
|
166
|
+
return false;
|
|
167
|
+
|
|
168
|
+
var builder = new StringBuilder();
|
|
169
|
+
while (index < json.Length)
|
|
170
|
+
{
|
|
171
|
+
var c = json[index++];
|
|
172
|
+
if (c == '"')
|
|
173
|
+
{
|
|
174
|
+
value = builder.ToString();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (c < ' ')
|
|
179
|
+
return false;
|
|
180
|
+
|
|
181
|
+
if (c != '\\')
|
|
182
|
+
{
|
|
183
|
+
builder.Append(c);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (index >= json.Length)
|
|
188
|
+
return false;
|
|
189
|
+
|
|
190
|
+
var escape = json[index++];
|
|
191
|
+
switch (escape)
|
|
192
|
+
{
|
|
193
|
+
case '"':
|
|
194
|
+
case '\\':
|
|
195
|
+
case '/':
|
|
196
|
+
builder.Append(escape);
|
|
197
|
+
break;
|
|
198
|
+
case 'b':
|
|
199
|
+
builder.Append('\b');
|
|
200
|
+
break;
|
|
201
|
+
case 'f':
|
|
202
|
+
builder.Append('\f');
|
|
203
|
+
break;
|
|
204
|
+
case 'n':
|
|
205
|
+
builder.Append('\n');
|
|
206
|
+
break;
|
|
207
|
+
case 'r':
|
|
208
|
+
builder.Append('\r');
|
|
209
|
+
break;
|
|
210
|
+
case 't':
|
|
211
|
+
builder.Append('\t');
|
|
212
|
+
break;
|
|
213
|
+
case 'u':
|
|
214
|
+
if (index + 4 > json.Length
|
|
215
|
+
|| !int.TryParse(json.Substring(index, 4), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var code))
|
|
216
|
+
{
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
builder.Append((char)code);
|
|
221
|
+
index += 4;
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
using UnityEditor;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
|
|
4
|
+
namespace GeneratedCodeDump.Editor
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Preferences page <c>Preferences/Generated Code Dump</c>: this user's switch for the generated-code dump,
|
|
8
|
+
/// and its output folder.
|
|
9
|
+
/// </summary>
|
|
10
|
+
internal sealed class GeneratedCodeDumpSettingsProvider : SettingsProvider
|
|
11
|
+
{
|
|
12
|
+
private static readonly string[] Keywords =
|
|
13
|
+
{
|
|
14
|
+
"Generated",
|
|
15
|
+
"Source Generator",
|
|
16
|
+
"Roslyn",
|
|
17
|
+
"Dump"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
private GUIStyle _panelStyle;
|
|
21
|
+
|
|
22
|
+
private GeneratedCodeDumpSettingsProvider()
|
|
23
|
+
: base(GeneratedCodeDumpIdentity.SettingsPath, SettingsScope.User, Keywords)
|
|
24
|
+
{
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[SettingsProvider]
|
|
28
|
+
private static SettingsProvider Create()
|
|
29
|
+
{
|
|
30
|
+
return new GeneratedCodeDumpSettingsProvider();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public override void OnGUI(string searchContext)
|
|
34
|
+
{
|
|
35
|
+
if (_panelStyle == null)
|
|
36
|
+
_panelStyle = new GUIStyle { margin = new RectOffset(10, 10, 6, 6) };
|
|
37
|
+
|
|
38
|
+
var labelWidth = EditorGUIUtility.labelWidth;
|
|
39
|
+
EditorGUIUtility.labelWidth = 170;
|
|
40
|
+
|
|
41
|
+
using (new EditorGUILayout.VerticalScope(_panelStyle))
|
|
42
|
+
{
|
|
43
|
+
EditorGUILayout.HelpBox(
|
|
44
|
+
"After each compile, replays the compiler of every rebuilt assembly and writes what its source "
|
|
45
|
+
+ "generators produced to " + GeneratedCodeDumpPaths.Root + "/<Assembly>/<Generator>/, so "
|
|
46
|
+
+ "tools and AI agents can read the real generated code.",
|
|
47
|
+
MessageType.None
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
DrawEnableSection();
|
|
51
|
+
DrawOutputSection();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
EditorGUIUtility.labelWidth = labelWidth;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static void DrawEnableSection()
|
|
58
|
+
{
|
|
59
|
+
EditorGUILayout.Space();
|
|
60
|
+
|
|
61
|
+
var current = GeneratedCodeDumpSettings.IsEnabled;
|
|
62
|
+
var enabled = EditorGUILayout.Toggle(new GUIContent("Dump generated code"), current);
|
|
63
|
+
if (enabled != current)
|
|
64
|
+
GeneratedCodeDumper.SetEnabled(enabled);
|
|
65
|
+
|
|
66
|
+
EditorGUILayout.HelpBox(
|
|
67
|
+
"Your own setting for this project: stored in " + GeneratedCodeDumpPaths.SettingsFile
|
|
68
|
+
+ " (keep UserSettings/ out of version control). An edit to that file applies within a second. Batch mode on this machine "
|
|
69
|
+
+ "follows it too.\n\nTurning it on, or Dump Now, runs a full dump: it replays every assembly of the "
|
|
70
|
+
+ "project, at most " + GeneratedCodeDumper.MaxConcurrent + " compilers at a time.",
|
|
71
|
+
MessageType.Info
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static void DrawOutputSection()
|
|
76
|
+
{
|
|
77
|
+
EditorGUILayout.Space();
|
|
78
|
+
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
|
|
79
|
+
EditorGUILayout.LabelField("Folder", GeneratedCodeDumpPaths.Root + "/");
|
|
80
|
+
|
|
81
|
+
using (new EditorGUILayout.HorizontalScope())
|
|
82
|
+
{
|
|
83
|
+
GUILayout.Space(EditorGUIUtility.labelWidth);
|
|
84
|
+
|
|
85
|
+
if (GUILayout.Button("Open Folder"))
|
|
86
|
+
GeneratedCodeMenuItems.OpenFolder();
|
|
87
|
+
|
|
88
|
+
if (GUILayout.Button("Dump Now"))
|
|
89
|
+
GeneratedCodeMenuItems.DumpNow();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Threading;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace GeneratedCodeDump.Editor
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Calls back, off the main thread, about 300 ms after the settings file is created, changed, renamed
|
|
10
|
+
/// or deleted, so an edit made outside the Editor applies even while the Editor sits in the background.
|
|
11
|
+
/// Not used in batch mode.
|
|
12
|
+
/// </summary>
|
|
13
|
+
/// <remarks>
|
|
14
|
+
/// Creates nothing: it watches UserSettings/ for <see cref="GeneratedCodeDumpPaths.SettingsFile"/>, and while
|
|
15
|
+
/// UserSettings/ is missing it watches the project root for that folder only (<see cref="Target"/>).
|
|
16
|
+
/// Callbacks never overlap, and <see cref="Stop"/> waits for a running one, so none outlives the domain.
|
|
17
|
+
/// </remarks>
|
|
18
|
+
internal static class GeneratedCodeDumpSettingsWatcher
|
|
19
|
+
{
|
|
20
|
+
private const int DebounceMilliseconds = 300;
|
|
21
|
+
private const int StopWaitMilliseconds = 2000;
|
|
22
|
+
|
|
23
|
+
// CallbackLock is taken before any of the dumper's locks; Sync is held only briefly, never while calling out.
|
|
24
|
+
private static readonly object CallbackLock = new object();
|
|
25
|
+
private static readonly object Sync = new object();
|
|
26
|
+
|
|
27
|
+
private static FileSystemWatcher _watcher;
|
|
28
|
+
private static string _watchedFolder;
|
|
29
|
+
private static Timer _debounce;
|
|
30
|
+
private static Action _onChange;
|
|
31
|
+
private static bool _stopped;
|
|
32
|
+
private static bool _warned;
|
|
33
|
+
|
|
34
|
+
internal static void Start(Action onChange)
|
|
35
|
+
{
|
|
36
|
+
lock (Sync)
|
|
37
|
+
{
|
|
38
|
+
if (_debounce != null)
|
|
39
|
+
return;
|
|
40
|
+
|
|
41
|
+
_onChange = onChange;
|
|
42
|
+
_stopped = false;
|
|
43
|
+
_debounce = new Timer(OnDebounced, null, Timeout.Infinite, Timeout.Infinite);
|
|
44
|
+
Arm();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
internal static void Stop()
|
|
49
|
+
{
|
|
50
|
+
lock (Sync)
|
|
51
|
+
{
|
|
52
|
+
_stopped = true;
|
|
53
|
+
_debounce?.Dispose();
|
|
54
|
+
_debounce = null;
|
|
55
|
+
DisposeWatcher();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Monitor.TryEnter(CallbackLock, StopWaitMilliseconds))
|
|
59
|
+
Monitor.Exit(CallbackLock);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// <summary>
|
|
63
|
+
/// The folder to watch and the name to watch in it for <paramref name="settingsFile"/> (a full path): its
|
|
64
|
+
/// folder for the file itself, or, while that folder is missing, the folder above for that folder's name.
|
|
65
|
+
/// </summary>
|
|
66
|
+
internal static (string Folder, string Filter) Target(string settingsFile, bool settingsFolderExists)
|
|
67
|
+
{
|
|
68
|
+
var settingsFolder = Path.GetDirectoryName(settingsFile);
|
|
69
|
+
return settingsFolderExists
|
|
70
|
+
? (settingsFolder, Path.GetFileName(settingsFile))
|
|
71
|
+
: (Path.GetDirectoryName(settingsFolder), Path.GetFileName(settingsFolder));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Under Sync.
|
|
75
|
+
private static void Arm()
|
|
76
|
+
{
|
|
77
|
+
if (_stopped)
|
|
78
|
+
return;
|
|
79
|
+
|
|
80
|
+
try
|
|
81
|
+
{
|
|
82
|
+
var settingsFile = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.SettingsFile);
|
|
83
|
+
var settingsFolder = Path.GetDirectoryName(settingsFile);
|
|
84
|
+
var settingsFolderExists = Directory.Exists(settingsFolder);
|
|
85
|
+
var (folder, filter) = Target(settingsFile, settingsFolderExists);
|
|
86
|
+
if (_watcher != null && string.Equals(_watchedFolder, folder, StringComparison.Ordinal))
|
|
87
|
+
return;
|
|
88
|
+
|
|
89
|
+
DisposeWatcher();
|
|
90
|
+
var watcher = new FileSystemWatcher(folder, filter)
|
|
91
|
+
{
|
|
92
|
+
IncludeSubdirectories = false,
|
|
93
|
+
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite | NotifyFilters.Size
|
|
94
|
+
| NotifyFilters.CreationTime
|
|
95
|
+
};
|
|
96
|
+
watcher.Changed += OnChanged;
|
|
97
|
+
watcher.Created += OnChanged;
|
|
98
|
+
watcher.Deleted += OnChanged;
|
|
99
|
+
watcher.Renamed += OnRenamed;
|
|
100
|
+
watcher.Error += OnError;
|
|
101
|
+
watcher.EnableRaisingEvents = true;
|
|
102
|
+
_watcher = watcher;
|
|
103
|
+
_watchedFolder = folder;
|
|
104
|
+
|
|
105
|
+
// The folder may have appeared between the check and the watcher starting.
|
|
106
|
+
if (!settingsFolderExists && Directory.Exists(settingsFolder))
|
|
107
|
+
Schedule();
|
|
108
|
+
}
|
|
109
|
+
catch (Exception exception)
|
|
110
|
+
{
|
|
111
|
+
Warn(exception);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static void DisposeWatcher()
|
|
116
|
+
{
|
|
117
|
+
var watcher = _watcher;
|
|
118
|
+
_watcher = null;
|
|
119
|
+
_watchedFolder = null;
|
|
120
|
+
if (watcher == null)
|
|
121
|
+
return;
|
|
122
|
+
|
|
123
|
+
try
|
|
124
|
+
{
|
|
125
|
+
watcher.EnableRaisingEvents = false;
|
|
126
|
+
watcher.Dispose();
|
|
127
|
+
}
|
|
128
|
+
catch (Exception)
|
|
129
|
+
{
|
|
130
|
+
// Already broken; nothing more to release.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static void OnChanged(object sender, FileSystemEventArgs args)
|
|
135
|
+
{
|
|
136
|
+
Schedule();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static void OnRenamed(object sender, RenamedEventArgs args)
|
|
140
|
+
{
|
|
141
|
+
Schedule();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static void OnError(object sender, ErrorEventArgs args)
|
|
145
|
+
{
|
|
146
|
+
// Re-created by the next callback, e.g. after the watched folder was deleted.
|
|
147
|
+
lock (Sync)
|
|
148
|
+
DisposeWatcher();
|
|
149
|
+
|
|
150
|
+
Schedule();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static void Schedule()
|
|
154
|
+
{
|
|
155
|
+
lock (Sync)
|
|
156
|
+
{
|
|
157
|
+
if (!_stopped)
|
|
158
|
+
_debounce?.Change(DebounceMilliseconds, Timeout.Infinite);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static void OnDebounced(object state)
|
|
163
|
+
{
|
|
164
|
+
lock (CallbackLock)
|
|
165
|
+
{
|
|
166
|
+
Action onChange;
|
|
167
|
+
lock (Sync)
|
|
168
|
+
{
|
|
169
|
+
if (_stopped)
|
|
170
|
+
return;
|
|
171
|
+
|
|
172
|
+
Arm();
|
|
173
|
+
onChange = _onChange;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try
|
|
177
|
+
{
|
|
178
|
+
onChange?.Invoke();
|
|
179
|
+
}
|
|
180
|
+
catch (Exception exception)
|
|
181
|
+
{
|
|
182
|
+
Warn(exception);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static void Warn(Exception exception)
|
|
188
|
+
{
|
|
189
|
+
if (_warned)
|
|
190
|
+
return;
|
|
191
|
+
|
|
192
|
+
_warned = true;
|
|
193
|
+
Debug.LogWarning(
|
|
194
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}watching {GeneratedCodeDumpPaths.SettingsFile} failed: {exception.Message} "
|
|
195
|
+
+ "An edit to it made outside the Editor now applies on the next compile."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|