Skip to content

Commit e88ea25

Browse files
dellis1972jonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] IOException & Console.InputEncoding (#4735)
Fixes: https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1130414 Context: 0d6fd83 For some unknown reason setting the [`Console.InputEncoding`][0] property on a customer's machine is throwing an `IOException`. This causes Visual Studio (Windows) to completely crash: Description: The process was terminated due to an unhandled exception. Exception Info: System.IO.IOException at System.IO.__Error.WinIOError(Int32, System.String) at System.IO.__Error.WinIOError() at System.Console.set_InputEncoding(System.Text.Encoding) at Xamarin.Android.Tasks.Aapt2Daemon.Aapt2DaemonStart() at System.Threading.ThreadHelper.ThreadStart_Context(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) at System.Threading.ThreadHelper.ThreadStart() The cause of the crash is that commit 0d6fd83 sets `Console.InputEncoding` to "UTF-8 without BOM" for communication with `aapt2`, so that we can sanely use Unicode paths, and setting `Console.InputEncoding` may result in a `System.IO.IOException`. (Paths are written to `Process.StandardInput`, which can contain Unicode characters. `Console.InputEncoding` on the other hand, defaults to "ANSI", which may not support full Unicode.) Why did 0d6fd83 need to set `Console.InputEncoding` in the first place? Because [`ProcessStartInfo.StandardInputEncoding`][1] wasn't added until .NET Standard 2.1, and .NET Framework <= 4.8 doesn't support .NET Standard 2.1, only .NET Standard 2.0. Furthermore, when `ProcessStartInfo.RedirectStandardInput` is True, [`Process.StandardInput` is a `StreamWriter`][2] which uses `Console.InputEncoding` as the encoding for the underlying stream. Thus, setting `Console.InputEncoding` is the only way in .NET Standard 2.0/.NET Framework <= 4.8 to control the encoding used by `Process.StandardInput`'s `StreamWriter`. Further exploration since 0d6fd83 suggests that we could create a new `StreamWriter` around `Process.StandardInput.BaseStream` with the correct "UTF-8 without BOM" encoding, but that approach fails on CI: …\Xamarin.Android.Aapt2.targets(124,3): error APT0001: Unknown option `-o`. Please check `$(AndroidAapt2CompileExtraArgs)` and `$(AndroidAapt2LinkExtraArgs)` to see if they include any `aapt` command line arguments that are no longer valid for `aapt2` and ensure that all other arguments are valid for `aapt2`. …\Xamarin.Android.Aapt2.targets(124,3): error APT0001: Unknown option `-o`. Please check `$(AndroidAapt2CompileExtraArgs)` and `$(AndroidAapt2LinkExtraArgs)` to see if they include any `aapt` command line arguments that are no longer valid for `aapt2` and ensure that all other arguments are valid for `aapt2`. …\Xamarin.Android.Aapt2.targets(124,3): error APT0001: Unknown option `-o`. Please check `$(AndroidAapt2CompileExtraArgs)` and `$(AndroidAapt2LinkExtraArgs)` to see if they include any `aapt` command line arguments that are no longer valid for `aapt2` and ensure that all other arguments are valid for `aapt2`. …\Xamarin.Android.Aapt2.targets(124,3): error APT0001: Unknown option `-o`. Please check `$(AndroidAapt2CompileExtraArgs)` and `$(AndroidAapt2LinkExtraArgs)` to see if they include any `aapt` command line arguments that are no longer valid for `aapt2` and ensure that all other arguments are valid for `aapt2`. We do not understand why this fails on CI unless `Console.InputEncoding` is previously set. All that said, we don't want Visual Studio to crash. Fix the crash by: 1. Using System.Reflection to probe for `ProcessStartInfo.StandardInputEncoding`. If it exists, use it. 2. If (2) fails, "fall back" and set `Console.InputEncoding` as before, wrapping in a `try`/`catch` block so that `IOException`s don't escape and crash Visual Studio. TODO: We need to figure out a way of reporting this exception somehow in the exception handler so we can get more data. Regardless of whether (1) or (2) fails, as part of writing to `aapt2`s `Process.StandardInput`, we wrap `Process.StandardInput.BaseStream` into a new `StreamWriter` which uses "UTF-8 without BOM" as the encoding. This is "defense in depth," though could plausibly result in the previously mentioned APT0001 errors if setting `Console.InputEncoding` failed. We'll see what happens "in the wild." [0]: https://docs.microsoft.com/en-us/dotnet/api/system.console.inputencoding?view=netcore-3.1 [1]: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.standardinputencoding?view=netcore-3.1 [2]: https://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,2154
1 parent d6e6062 commit e88ea25

File tree

1 file changed

+48
-18
lines changed

1 file changed

+48
-18
lines changed

src/Xamarin.Android.Build.Tasks/Utilities/Aapt2Daemon.cs

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.IO;
66
using System.Text;
77
using System.Threading;
8+
using System.Reflection;
89
using Microsoft.Build.Framework;
910
using TPL = System.Threading.Tasks;
11+
using Xamarin.Android.Tools;
1012

1113
namespace Xamarin.Android.Tasks
1214
{
@@ -144,6 +146,33 @@ public void Stop ()
144146
pendingJobs.CompleteAdding ();
145147
}
146148

149+
private bool SetConsoleInputEncoding (Encoding encoding)
150+
{
151+
try {
152+
if (Console.InputEncoding != encoding) {
153+
Console.InputEncoding = encoding;
154+
return true;
155+
}
156+
} catch (IOException) {
157+
//In a DesignTime Build on VS Windows sometimes this exception is raised.
158+
//We should catch it, but there is nothing we can do about it.
159+
}
160+
return false;
161+
}
162+
163+
private bool SetProcessInputEncoding (ProcessStartInfo info, Encoding encoding)
164+
{
165+
Type type = info.GetType ();
166+
PropertyInfo prop = type.GetRuntimeProperty ("StandardInputEncoding");
167+
if (prop == null)
168+
prop = type.GetProperty ("StandardInputEncoding", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
169+
if(prop?.CanWrite ?? false) {
170+
prop.SetValue (info, encoding, null);
171+
return true;
172+
}
173+
return false;
174+
}
175+
147176
private void Aapt2DaemonStart ()
148177
{
149178
ProcessStartInfo info = new ProcessStartInfo (Aapt2)
@@ -158,23 +187,24 @@ private void Aapt2DaemonStart ()
158187
WorkingDirectory = Path.GetTempPath (),
159188
StandardErrorEncoding = Encoding.UTF8,
160189
StandardOutputEncoding = Encoding.UTF8,
190+
// We need to FORCE the StandardInput to be UTF8 so we can use
191+
// accented characters. Also DONT INCLUDE A BOM!!
192+
// otherwise aapt2 will try to interpret the BOM as an argument.
161193
// Cant use this cos its netstandard 2.1 only
162194
// and we are using netstandard 2.0
163-
//StandardInputEncoding = Encoding.UTF8,
195+
//StandardInputEncoding = MonoAndroidHelper.UTF8withoutBOM,
164196
};
165-
// We need to FORCE the StandardInput to be UTF8 so we can use
166-
// accented characters. Also DONT INCLUDE A BOM!!
167-
// otherwise aapt2 will try to interpret the BOM as an argument.
168197
Process aapt2;
169-
lock (lockObject) {
170-
Encoding current = Console.InputEncoding;
198+
Encoding currentEncoding = Console.InputEncoding;
199+
lock (lockObject) {
171200
try {
172-
Console.InputEncoding = new UTF8Encoding (false);
201+
if (!SetProcessInputEncoding (info, MonoAndroidHelper.UTF8withoutBOM))
202+
SetConsoleInputEncoding (MonoAndroidHelper.UTF8withoutBOM);
173203
aapt2 = new Process ();
174204
aapt2.StartInfo = info;
175205
aapt2.Start ();
176206
} finally {
177-
Console.InputEncoding = current;
207+
SetConsoleInputEncoding (currentEncoding);
178208
}
179209
}
180210
try {
@@ -183,15 +213,15 @@ private void Aapt2DaemonStart ()
183213
bool errored = false;
184214
try {
185215
// try to write Unicode UTF8 to aapt2
186-
StreamWriter writer = aapt2.StandardInput;
187-
foreach (var arg in job.Commands)
188-
{
189-
writer.WriteLine (arg);
216+
using (StreamWriter writer = new StreamWriter (aapt2.StandardInput.BaseStream, MonoAndroidHelper.UTF8withoutBOM, bufferSize: 1024, leaveOpen: true)) {
217+
foreach (var arg in job.Commands) {
218+
writer.WriteLine (arg);
219+
}
220+
writer.WriteLine ();
221+
writer.Flush ();
190222
}
191-
writer.WriteLine ();
192-
writer.Flush ();
193223
string line;
194-
224+
195225
Queue<string> stdError = new Queue<string> ();
196226
while ((line = aapt2.StandardError.ReadLine ()) != null) {
197227
if (string.Compare (line, "Done", StringComparison.OrdinalIgnoreCase) == 0) {
@@ -201,8 +231,8 @@ private void Aapt2DaemonStart ()
201231
errored = true;
202232
continue;
203233
}
204-
// we have to queue the output because the "Done"/"Error" lines are
205-
//written after all the messages. So to process the warnings/errors
234+
// we have to queue the output because the "Done"/"Error" lines are
235+
//written after all the messages. So to process the warnings/errors
206236
// correctly we need to do this after we know if worked or failed.
207237
stdError.Enqueue (line);
208238
}
@@ -256,4 +286,4 @@ bool IsAapt2Warning (string singleLine)
256286
return false;
257287
}
258288
}
259-
}
289+
}

0 commit comments

Comments
 (0)