Skip to content

Commit fbb0f72

Browse files
Analyze command - Improvements in error handling and logging (#34)
* Modest improvement in error reporting and Archive handling Check for Archives rather than trying to mount every file just in case it turns out to be an archive. Unity Archives have a distinctive signature string at the start so we can pre-triage which files are actually Unity Archives. The expected values were confirmed by studying the Unity parsing code and debugging the load of some files in the editor. Output improvement - when erasing progress message move back to the start of the line so that errors are not printed at a random indent. This is only a modest improvement because we still don't have some mechanism to predict if a file is a Unity Serialized File without attempting to load it and then failing. * Analyze - summarize number of successful, ignored and failed files Previously it was hard to tell if errors were shown whether anything actually had been analyzed. Partial imports are normal until we find a way to reliably detect serialized files. * Document more expected errors from Analyze Mention errors that migth be visible (based on testing a folder with AssetBundles from different builds all being mixed together)
1 parent c763f6e commit fbb0f72

File tree

2 files changed

+145
-56
lines changed

2 files changed

+145
-56
lines changed

Analyzer/AnalyzerTool.cs

Lines changed: 107 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,35 @@ public int Analyze(
4141
searchPattern,
4242
noRecursion ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
4343

44+
int countFailures = 0;
45+
int countSuccess = 0;
46+
int countIgnored = 0;
4447
int i = 1;
4548
foreach (var file in files)
4649
{
4750
if (ShouldIgnoreFile(file))
4851
{
49-
var relativePath = Path.GetRelativePath(path, file);
50-
5152
if (m_Verbose)
5253
{
54+
var relativePath = Path.GetRelativePath(path, file);
5355
Console.WriteLine();
5456
Console.WriteLine($"Ignoring {relativePath}");
5557
}
56-
++i;
57-
continue;
58+
countIgnored++;
59+
}
60+
else if (!ProcessFile(file, path, writer, i, files.Length))
61+
{
62+
countFailures++;
63+
}
64+
else
65+
{
66+
countSuccess++;
5867
}
59-
60-
ProcessFile(file, path, writer, i, files.Length);
6168
++i;
6269
}
6370

6471
Console.WriteLine();
65-
Console.WriteLine("Finalizing database...");
72+
Console.WriteLine($"Finalizing database. Successfully processed files: {countSuccess}, Failed files: {countFailures}, Ignored files: {countIgnored}");
6673

6774
writer.End();
6875

@@ -98,69 +105,83 @@ bool ShouldIgnoreFile(string file)
98105

99106
private static readonly HashSet<string> IgnoredExtensions = new()
100107
{
101-
".txt", ".resS", ".resource", ".json", ".dll", ".pdb", ".exe", ".manifest", ".entities", ".entityheader"
108+
".txt", ".resS", ".resource", ".json", ".dll", ".pdb", ".exe", ".manifest", ".entities", ".entityheader",
109+
".ini", ".config"
102110
};
103111

104-
void ProcessFile(string file, string rootDirectory, SQLiteWriter writer, int fileIndex, int cntFiles)
112+
bool ProcessFile(string file, string rootDirectory, SQLiteWriter writer, int fileIndex, int cntFiles)
105113
{
114+
bool successful = true;
106115
try
107116
{
108-
UnityArchive archive = null;
109-
110-
try
117+
if (IsUnityArchive(file))
111118
{
112-
archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar);
113-
}
114-
catch (NotSupportedException)
115-
{
116-
// It wasn't an AssetBundle, try to open the file as a SerializedFile.
117-
118-
var relativePath = Path.GetRelativePath(rootDirectory, file);
119-
writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file));
120-
121-
ReportProgress(relativePath, fileIndex, cntFiles);
122-
}
123-
124-
if (archive != null)
125-
{
126-
try
119+
using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar))
127120
{
128-
var assetBundleName = Path.GetRelativePath(rootDirectory, file);
129-
130-
writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length);
131-
ReportProgress(assetBundleName, fileIndex, cntFiles);
121+
if (archive == null)
122+
throw new FileLoadException($"Failed to mount archive: {file}");
132123

133-
foreach (var node in archive.Nodes)
124+
try
134125
{
135-
if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile))
126+
var assetBundleName = Path.GetRelativePath(rootDirectory, file);
127+
128+
writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length);
129+
ReportProgress(assetBundleName, fileIndex, cntFiles);
130+
131+
foreach (var node in archive.Nodes)
136132
{
137-
try
133+
if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile))
138134
{
139-
writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file));
140-
}
141-
catch (Exception e)
142-
{
143-
EraseProgressLine();
144-
Console.Error.WriteLine($"Error processing {node.Path} in archive {file}");
145-
Console.Error.WriteLine(e);
146-
Console.WriteLine();
135+
try
136+
{
137+
writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file));
138+
}
139+
catch (Exception e)
140+
{
141+
// the most likely exception here is Microsoft.Data.Sqlite.SqliteException,
142+
// for example 'UNIQUE constraint failed: serialized_files.id'.
143+
// or 'UNIQUE constraint failed: objects.id' which can happen
144+
// if AssetBundles from different builds are being processed by a single call to Analyze
145+
// or if there is a Unity Data Tool bug.
146+
EraseProgressLine();
147+
Console.Error.WriteLine($"Error processing {node.Path} in archive {file}");
148+
Console.Error.WriteLine(e.Message);
149+
Console.WriteLine();
150+
151+
// It is possible some files inside an archive will pass and others will fail, to have a partial analyze.
152+
// Overall that is reported as a failure
153+
successful = false;
154+
}
147155
}
148156
}
149157
}
150-
}
151-
finally
152-
{
153-
writer.EndAssetBundle();
154-
archive.Dispose();
158+
finally
159+
{
160+
writer.EndAssetBundle();
161+
}
155162
}
156163
}
164+
else
165+
{
166+
// This isn't a Unity Archive file. Try to open it as a SerializedFile.
167+
// Unfortunately there is no standard file extension, or clear signature at the start of the file,
168+
// to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files,
169+
// and there is a chance for crashes and freezes if the parser misinterprets the file content.
170+
var relativePath = Path.GetRelativePath(rootDirectory, file);
171+
writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file));
172+
173+
ReportProgress(relativePath, fileIndex, cntFiles);
174+
}
175+
157176
EraseProgressLine();
158177
}
159178
catch (NotSupportedException)
160179
{
161180
EraseProgressLine();
162181
Console.Error.WriteLine();
163182
//A "failed to load" error will already be logged by the UnityFileSystem library
183+
184+
successful = false;
164185
}
165186
catch (Exception e)
166187
{
@@ -170,9 +191,47 @@ void ProcessFile(string file, string rootDirectory, SQLiteWriter writer, int fil
170191
Console.WriteLine($"{e.GetType()}: {e.Message}");
171192
if (m_Verbose)
172193
Console.WriteLine(e.StackTrace);
194+
195+
successful = false;
196+
}
197+
198+
return successful;
199+
}
200+
201+
private static bool IsUnityArchive(string filePath)
202+
{
203+
// Check whether a file is a Unity Archive (AssetBundle) by looking for known signatures at the start of the file.
204+
// "UnifyFS" is the current signature, but some older formats of the file are still supported
205+
string[] signatures = { "UnityFS", "UnityWeb", "UnityRaw", "UnityArchive" };
206+
int maxLen = 12; // "UnityArchive".Length
207+
byte[] buffer = new byte[maxLen];
208+
209+
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
210+
{
211+
int read = fs.Read(buffer, 0, buffer.Length);
212+
foreach (var sig in signatures)
213+
{
214+
if (read >= sig.Length)
215+
{
216+
bool match = true;
217+
for (int i = 0; i < sig.Length; ++i)
218+
{
219+
if (buffer[i] != sig[i])
220+
{
221+
match = false;
222+
break;
223+
}
224+
}
225+
if (match)
226+
return true;
227+
}
228+
}
229+
return false;
173230
}
174231
}
175232

233+
234+
176235
int m_LastProgressMessageLength = 0;
177236

178237
void ReportProgress(string relativePath, int fileIndex, int cntFiles)
@@ -195,7 +254,7 @@ void ReportProgress(string relativePath, int fileIndex, int cntFiles)
195254
void EraseProgressLine()
196255
{
197256
if (!m_Verbose)
198-
Console.Write($"\r{new string(' ', m_LastProgressMessageLength)}");
257+
Console.Write($"\r{new string(' ', m_LastProgressMessageLength)}\r");
199258
else
200259
Console.WriteLine();
201260
}

UnityDataTool/README.md

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,6 @@ Example: `UnityDataTool analyze /path/to/asset/bundles -o my_database.db -p "*.b
6262
**Refer to this [documentation](../Analyzer/README.md#How-to-use-the-database) for more information
6363
about the output database structure.**
6464

65-
Note: If a SerializedFile is built without TypeTrees, then the command will not be able to extract information about the contained objects. It will print an error similar to this example, then skip to the next file:
66-
67-
```
68-
Error processing file: C:\Src\TestProject\Build\Player\TestProject_Data\level0
69-
System.ArgumentException: Invalid object id.
70-
```
71-
72-
See [this topic](../Documentation/unity-content-format.md) for more information about TypeTrees.
7365

7466
### Example Input to the Analyze command
7567

@@ -104,6 +96,44 @@ For Player builds there is no single -p option that can catch all SerializedFile
10496

10597
The `--no-recurse` option can reduce the volume of these warnings.
10698

99+
100+
### Errors when TypeTrees are missing
101+
102+
If a SerializedFile is built without TypeTrees, then the Analyze command will not be able to extract information about the contained objects. It will print an error similar to this example, then skip to the next file:
103+
104+
```
105+
Error processing file: C:\Src\TestProject\Build\Player\TestProject_Data\level0
106+
System.ArgumentException: Invalid object id.
107+
```
108+
109+
See [this topic](../Documentation/unity-content-format.md) for more information about TypeTrees.
110+
111+
### SQL Errors
112+
113+
The following SQL errors may occur when running the analyze command:
114+
115+
```
116+
SQLite Error 19: 'UNIQUE constraint failed: objects.id'
117+
```
118+
119+
or
120+
121+
```
122+
SQLite Error 19: 'UNIQUE constraint failed: serialized_files.id'.
123+
```
124+
125+
The likely cause of these errors is the same serialized file name appearing in more than Player or AssetBundle file that is processed by Analyze.
126+
127+
This may occur:
128+
129+
* If you analyze files from more than one version of the same build (e.g. if you run it on a directory that contains two different builds of the same project in separate sub-directories).
130+
* If two scenes with the same filename (but different paths) are included in a build.
131+
* In a build that used AssetBundle variants.
132+
133+
The conflicting name makes it impossible to uniquely identify the serialized file and its object in the database, and makes it ambiguous how to interpret dependencies from one file to another.
134+
135+
The [comparing builds](../Documentation/comparing-builds.md) topic gives some ideas about how to run Analyze more than once if you want to compare two different versions of the same build.
136+
107137
## dump
108138

109139
This command dumps the contents of a SerializedFile into a file of the selected format. It currently

0 commit comments

Comments
 (0)