Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP, basic infrastructure #213

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<Import Project="MSBuildReferences.projitems" />

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net7.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<RootNamespace>Microsoft.Android.Build.Tasks</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SignAssembly>true</SignAssembly>
Expand Down
88 changes: 88 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/AsyncProcessRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Xamarin.Android.Tools;

class AsyncProcessRunner : ProcessRunner
{
public AsyncProcessRunner (Action<TraceLevel, string>? logger = null)
: base (logger)
{}

public async Task<ProcessExitState> RunAsync (string executablePath, params string[] arguments)
{
return await DoRun (executablePath, arguments);
}

public async Task<ProcessExitState> RunAsync (CancellationToken cancellationToken, string executablePath, params string[] arguments)
{
return await DoRun (executablePath, arguments, cancellationToken: cancellationToken);
}

public async Task<ProcessExitState> RunAsync (string executablePath, ICollection<string> arguments)
{
return await DoRun (executablePath, arguments);
}

public async Task<ProcessExitState> RunAsync (CancellationToken cancellationToken, string executablePath, ICollection<string> arguments)
{
return await DoRun (executablePath, arguments, cancellationToken: cancellationToken);
}

public async Task<ProcessExitState> RunAsync (ProcessRunOptions? runOptions, string executablePath, params string[] arguments)
{
return await DoRun (executablePath, arguments, runOptions);
}

public async Task<ProcessExitState> RunAsync (CancellationToken cancellationToken, ProcessRunOptions? runOptions, string executablePath, params string[] arguments)
{
return await DoRun (executablePath, arguments, runOptions, cancellationToken);
}

public async Task<ProcessExitState> RunAsync (ProcessRunOptions? runOptions, string executablePath, ICollection<string> arguments)
{
return await DoRun (executablePath, arguments, runOptions);
}

public async Task<ProcessExitState> RunAsync (CancellationToken cancellationToken, ProcessRunOptions? runOptions, string executablePath, ICollection<string> arguments)
{
return await DoRun (executablePath, arguments, runOptions, cancellationToken);
}

async Task<ProcessExitState> DoRun (string executablePath, ICollection<string>? arguments, ProcessRunOptions? runOptions = null, CancellationToken cancellationToken = default)
{
var state = new RunState (runOptions ?? CreateDefaultRunOptions ()) {
ThreadSafe = true,
};

if (cancellationToken == default) {
return await DoRunInner (state, executablePath, arguments, cancellationToken);
}

ProcessExitState exitState;

// If the token is cancelled while we're running, kill the process.
// Otherwise once we finish the Task.WhenAll we can remove this registration
// as there is no longer any need to Kill the process.
using (cancellationToken.Register (() => state.KillProcess ())) {
exitState = await DoRunInner (state, executablePath, arguments, cancellationToken);
}

// If we invoke 'KillProcess' our output, error and exit tasks will all complete normally.
// To protected against passing the user incomplete data we have to call
// `cancellationToken.ThrowIfCancellationRequested ()` here.
cancellationToken.ThrowIfCancellationRequested ();
return exitState;
}

Task<ProcessExitState> DoRunInner (RunState state, string executablePath, ICollection<string>? arguments, CancellationToken cancellationToken = default)
{
return Task.Run<ProcessExitState> (
() => RunInner (state, executablePath, arguments),
cancellationToken
);
}
}
109 changes: 58 additions & 51 deletions src/Xamarin.Android.Tools.AndroidSdk/JdkInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,57 +259,58 @@ static bool AnySystemJavasInstalled ()

static Dictionary<string, List<string>> GetJavaProperties (Action<TraceLevel, string> logger, string java)
{
var javaProps = new ProcessStartInfo {
FileName = java,
Arguments = "-XshowSettings:properties -version",
};

var props = new Dictionary<string, List<string>> ();
string? curKey = null;
bool foundPS = false;
var output = new StringBuilder ();

if (!AnySystemJavasInstalled () && (java == "/usr/bin/java" || java == "java"))
return props;

const string PropertySettings = "Property settings:";

ProcessUtils.Exec (javaProps, (o, e) => {
using var outputWriter = new ProcessStringWriter {
OnLineReceived = (string? line) => {
const string ContinuedValuePrefix = " ";
const string NewValuePrefix = " ";
const string NameValueDelim = " = ";
output.AppendLine (e.Data);
if (string.IsNullOrEmpty (e.Data))
return;
if (e.Data.StartsWith (PropertySettings, StringComparison.Ordinal)) {
if (string.IsNullOrEmpty (line)) {
return false;
}
if (line!.StartsWith (PropertySettings, StringComparison.Ordinal)) {
foundPS = true;
return;
return true;
}
if (!foundPS) {
return;
return true;
}
if (e.Data.StartsWith (ContinuedValuePrefix, StringComparison.Ordinal)) {
if (line.StartsWith (ContinuedValuePrefix, StringComparison.Ordinal)) {
if (curKey == null) {
logger (TraceLevel.Error, $"No Java property previously seen for continued value `{e.Data}`.");
return;
logger (TraceLevel.Error, $"No Java property previously seen for continued value `{line}`.");
return true;
}
props [curKey].Add (e.Data.Substring (ContinuedValuePrefix.Length));
return;
props [curKey].Add (line.Substring (ContinuedValuePrefix.Length));
return true;
}
if (e.Data.StartsWith (NewValuePrefix, StringComparison.Ordinal)) {
var delim = e.Data.IndexOf (NameValueDelim, StringComparison.Ordinal);
if (delim <= 0)
return;
curKey = e.Data.Substring (NewValuePrefix.Length, delim - NewValuePrefix.Length);
var value = e.Data.Substring (delim + NameValueDelim.Length);
if (line.StartsWith (NewValuePrefix, StringComparison.Ordinal)) {
var delim = line.IndexOf (NameValueDelim, StringComparison.Ordinal);
if (delim <= 0) {
return true;
}
curKey = line.Substring (NewValuePrefix.Length, delim - NewValuePrefix.Length);
var value = line.Substring (delim + NameValueDelim.Length);
List<string>? values;
if (!props.TryGetValue (curKey!, out values))
if (!props.TryGetValue (curKey!, out values)) {
props.Add (curKey, values = new List<string> ());
}
values.Add (value);
}
});

return true;
},
};
ProcessUtils.Exec (stdoutWriter: outputWriter, stderrWriter: outputWriter, java, "-XshowSettings:properties", "-version");

if (!foundPS) {
logger (TraceLevel.Warning, $"No Java properties found; did not find `{PropertySettings}` in `{java} -XshowSettings:properties -version` output: ```{output.ToString ()}```");
logger (TraceLevel.Warning, $"No Java properties found; did not find `{PropertySettings}` in `{java} -XshowSettings:properties -version` output: ```{outputWriter.ToString ()}```");
}

return props;
Expand Down Expand Up @@ -387,22 +388,24 @@ static IEnumerable<string> GetLibexecJdkPaths (Action<TraceLevel, string> logger
if (!File.Exists (java_home)) {
yield break;
}
var jhp = new ProcessStartInfo {
FileName = java_home,
Arguments = "-X",
using var xmlWriter = new ProcessStringWriter {
OnLineReceived = (string? line) => {
if (string.IsNullOrEmpty (line)) {
return false;
}

return true;
},
};
var xml = new StringBuilder ();
ProcessUtils.Exec (jhp, (o, e) => {
if (string.IsNullOrEmpty (e.Data))
return;
xml.Append (e.Data);
}, includeStderr: false);

ICollection<string> args = new string[] { "-X" };
ProcessUtils.Exec (stdoutWriter: xmlWriter, java_home, args);

XElement plist;
try {
plist = XElement.Parse (xml.ToString ());
plist = XElement.Parse (xmlWriter.ToString ());
} catch (XmlException e) {
logger (TraceLevel.Warning, string.Format (Resources.InvalidXmlLibExecJdk_path_args_message, jhp.FileName, jhp.Arguments, e.Message));
logger (TraceLevel.Warning, string.Format (Resources.InvalidXmlLibExecJdk_path_args_message, java_home, String.Join (" ", args), e.Message));
yield break;
}
foreach (var info in plist.Elements ("array").Elements ("dict")) {
Expand Down Expand Up @@ -433,21 +436,25 @@ static IEnumerable<string> GetJavaAlternativesJdkPaths ()
if (!File.Exists (alternatives))
return Enumerable.Empty<string> ();

var psi = new ProcessStartInfo {
FileName = alternatives,
Arguments = "-l",
};
var alternativesSplit = new[]{ ' ' };
var paths = new List<string> ();
ProcessUtils.Exec (psi, (o, e) => {
if (string.IsNullOrWhiteSpace (e.Data))
return;
var outputWriter = new NoProcesssOutputWriter {
OnLineReceived = (string? line) => {
if (string.IsNullOrWhiteSpace (line)) {
return false;
}

// Example line:
// java-1.8.0-openjdk-amd64 1081 /usr/lib/jvm/java-1.8.0-openjdk-amd64
var columns = e.Data.Split (new[]{ ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (columns.Length <= 2)
return;
paths.Add (columns [2]);
});
var columns = line!.Split (alternativesSplit, StringSplitOptions.RemoveEmptyEntries);
if (columns.Length > 2) {
paths.Add (columns [2]);
}
return true;
},
};

ProcessUtils.Exec (stdoutWriter: outputWriter, alternatives, "-l");
return paths;
}

Expand Down