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

Cli parser fixups #116

Merged
merged 9 commits into from
Sep 29, 2020
9 changes: 6 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
"preLaunchTask": "", //"build",
"program": "${workspaceFolder}/out/dotnet/source/Mlos.Agent.Server/objd/AnyCPU/Mlos.Agent.Server.dll",
"args": [
//"--optimizer-uri",
//"http://localhost:50051",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixup for the default debugger experience.
If this options are enabled by default, the command will fail.

In the future we can consider to add a preLaunchTask that starts the optimizer service.

"--executable",
"${workspaceFolder}/out/cmake/Debug/source/Mlos.UnitTest/Mlos.UnitTest"
],
"env": {
Expand All @@ -42,10 +45,10 @@
"preLaunchTask": "", //"build",
"program": "${workspaceFolder}/out/dotnet/source/Mlos.Agent.Server/objd/AnyCPU/Mlos.Agent.Server.dll",
"args": [
//"--optimizer-uri",
//"http://localhost:50051",
"--executable",
"${workspaceFolder}/out/cmake/Debug/source/Examples/SmartCache/SmartCache",
"--optimizer-uri",
"http://localhost:50051"
"${workspaceFolder}/out/cmake/Debug/source/Examples/SmartCache/SmartCache"
],
"env": {
"MLOS_SETTINGS_REGISTRY_PATH": "${workspaceFolder}/out/dotnet/source/Examples/SmartCache/SmartCache.SettingsRegistry/objd/AnyCPU"
Expand Down
158 changes: 158 additions & 0 deletions source/Mlos.Agent.Server/CliOptionsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// -----------------------------------------------------------------------
// <copyright file="CliOptionsParser.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root
// for license information.
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;

using CommandLine;
using CommandLine.Text;

namespace Mlos.Agent.Server
{
/// <summary>
/// A helper class for parsing the command line arguments fed to <see cref="MlosAgentServer.Main" /> and displaying help usage
/// output if necessary.
/// </summary>
public static class CliOptionsParser
{
/// <summary>
/// Parses the command line arguments fed to <see cref="MlosAgentServer.Main" />.
/// Displays help output and forces a program exit upon errors.
/// </summary>
/// <param name="args">The input arguments to parse.</param>
/// <param name="executablePath">The path to the executable found in the cli args.</param>
/// <param name="optimizerUri">The optimizer uri found in the cli args.</param>
public static void ParseArgs(string[] args, out string executablePath, out Uri optimizerUri)
{
string executableFilePath = null;
Uri optimizerAddressUri = null;

IEnumerable<string> extraArgs = null;

var cliOptsParser = new Parser(with => with.HelpWriter = null);
var cliOptsParseResult = cliOptsParser.ParseArguments<CliOptions>(args)
.WithParsed(parsedOptions =>
{
executableFilePath = parsedOptions.Executable;
optimizerAddressUri = parsedOptions.OptimizerUri;
extraArgs = parsedOptions.ExtraArgs;
});
if (cliOptsParseResult.Tag == ParserResultType.NotParsed)
{
cliOptsParseResult.WithNotParsed(errs => ShowUsageHelp(
cliOptsParseResult,
errors: errs,
msg: "Failed to parse command line options."));
}
else if (extraArgs != null && extraArgs.Any())
{
ShowUsageHelp(cliOptsParseResult, msg: "ERROR: Unknown arguments: " + string.Join(" ", extraArgs));
}

cliOptsParser.Dispose();

// Populate the output variables
//
executablePath = executableFilePath;
optimizerUri = optimizerAddressUri;
}

/// <summary>
/// Displays the help usage, possibly with some error messages, and then exits (non-zero).
/// </summary>
/// <param name="parserResult">The results from a CommandLine.Parser.ParseArguments() operation.</param>
/// <param name="errors">An errors reported by the parserResult.</param>
/// <param name="msg">An optional error message to accompany the output.</param>
private static void ShowUsageHelp<T>(ParserResult<T> parserResult, IEnumerable<Error> errors = null, string msg = null)
{
if (msg != null)
{
Console.Error.WriteLine(msg);
Console.Error.WriteLine();
}

if (errors == null)
{
errors = new List<Error>();
}

HelpText helpText = null;
if (errors.IsVersion())
{
helpText = HelpText.AutoBuild(parserResult);
}
else
{
helpText = HelpText.AutoBuild(
parserResult,
onError: ht =>
{
return HelpText.DefaultParsingErrorsHandler(parserResult, ht);
},
e => e);
helpText.AddNewLineBetweenHelpSections = true;
helpText.AddPreOptionsLines(new[]
{
string.Empty,

// Use a single long line of text to let the help output get wrapped automatically for us.
"The Mlos.Agent.Server acts as an external agent for MLOS integrated components, allowing them to "
+ "send it messages over shared memory, which it can process and use to interface with an optimizer "
+ "service to tune the components over their shared memory communication channels.",
string.Empty,

// Indent the actual commands to make them stand out a bit more.
// Note: The help out preserves the indent across wrapping.
"usage mode 1: Wait for an application to register over global shared memory, without an optimizer.",
" dotnet Mlos.Agent.Server.dll",
string.Empty,

"usage mode 2: Wait for an application to register over global shared memory, and prepare to "
+ "communicate with an MLOS optimizer listening at the given Grpc URI.",
" dotnet Mlos.Agent.Server.dll --optimizer-uri http://localhost:50051",
string.Empty,

"usage mode 3: Start an executable to communicate over freshly prepared global shared memory.",
" dotnet Mlos.Agent.Server.dll --executable path/to/executable",
string.Empty,

"usage mode 4: Start an executable to communicate over freshly prepared global shared memory and "
+ "prepare to communicate with an MLOS optimizer listening at the given Grpc URI.",
" dotnet Mlos.Agent.Server.dll --executable path/to/executable --optimizer-uri http://localhost:50051",
string.Empty,

"Note: the optimizer service used in these examples can be started using the 'start_optimizer_microservice "
+ "launch --port 50051' command from the mlos Python module.",
});
}

Console.WriteLine(helpText);
Environment.Exit(1);
}

/// <summary>
/// The command line options for this application.
/// </summary>
private class CliOptions
{
[Option("executable", Required = false, Default = null, HelpText = "A path to an executable to start (e.g. 'target/bin/Release/SmartCache').")]
public string Executable { get; set; }

[Option("optimizer-uri", Required = false, Default = null, HelpText = "A URI to connect to the MLOS Optimizer service over GRPC (e.g. 'http://localhost:50051').")]
public Uri OptimizerUri { get; set; }

/// <remarks>
/// Just used to detect any extra arguments so we can throw a warning.
/// See Also: https://github.com/microsoft/MLOS/issues/112.
/// </remarks>
[CommandLine.Value(0)]
public IEnumerable<string> ExtraArgs { get; set; }
}
}
}
1 change: 1 addition & 0 deletions source/Mlos.Agent.Server/Mlos.Agent.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ProjectReference Include="$(SourceDir)\Mlos.NetCore\Mlos.NetCore.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="CliOptionsParser.cs" />
<Compile Include="MlosAgentServer.cs" />
<Compile Include="TargetProcessManager.cs" />
</ItemGroup>
Expand Down
29 changes: 1 addition & 28 deletions source/Mlos.Agent.Server/MlosAgentServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
using System.Threading;
using System.Threading.Tasks;

using CommandLine;

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -57,20 +55,7 @@ public static void Main(string[] args)
{
string executableFilePath = null;
Uri optimizerAddressUri = null;

var cliOptsParseResult = CommandLine.Parser.Default.ParseArguments<CliOptions>(args)
.WithParsed(parsedOptions =>
{
executableFilePath = parsedOptions.Executable;
optimizerAddressUri = parsedOptions.OptimizerUri;
});
if (cliOptsParseResult.Tag == ParserResultType.NotParsed)
{
// CommandLine already prints the help text for us in this case.
//
Console.Error.WriteLine("Failed to parse command line options.");
Environment.Exit(1);
}
CliOptionsParser.ParseArgs(args, out executableFilePath, out optimizerAddressUri);

// Check for the executable before setting up any shared memory to
// reduce cleanup issues.
Expand Down Expand Up @@ -219,17 +204,5 @@ public static void Main(string[] args)

Console.WriteLine("Mlos.Agent exited.");
}

/// <summary>
/// The command line options for this application.
/// </summary>
private class CliOptions
{
[Option("executable", Required = false, Default = null, HelpText = "A path to an executable to start (e.g. 'target/bin/Release/SmartCache').")]
public string Executable { get; set; }

[Option("optimizer-uri", Required = false, Default = null, HelpText = "A URI to connect to the MLOS Optimizer service over GRPC (e.g. 'http://localhost:50051').")]
public Uri OptimizerUri { get; set; }
}
}
}