Skip to content

Commit

Permalink
Add Plugin CLI Tool (#317)
Browse files Browse the repository at this point in the history
* Add CLI

* Add push

* Update assembly name

* Add json schema doc

* Add components doc

* Add CLI doc

* Fix doc

* Fix doc 2

* wip

* Fix metadata generator

* Update package extension to .ptix

* Remove old projeact

* Delete and untrack launchsettings.json

* Print errors

* Update extension name to ptix

* Require Id and Version

* Put cli and publisher into a folder and don't interfere with plugins system versioning

* Add manifest schema

* Exclude schema folder

* Update plugin project template

* Update metadata gen

* Clean up metadata generation

* Fix schema comma

* Add manifest schema validation

* Validate using data annotations

* Update pack

* Pack as dotnet tool

* Change publisher location

* Resolve conflicts

* Fix contents metadata

* Remove version from schema

* Add manifestVersion to required

* Move package stuff to its own project

* Fix issues in cli

* Update plugin template

* Undo change in .gitignore

* Untrack launchSettings.json

* Update to use json validation

* Update schema location

* Add "bundled manifest" option

* Fix shouldinclude

* Update template

* Update plugin template

* Rename template

* Fix path issues

* Revert changes in templates and samples

* Remove publisher project

* Remove added files in samples

* Remove schema folder

* Remove schema md doc

* Add comments in validator

* Update source directory validation to check SDK

* Refactor options classes

* Handles destination overwrites

* Undo package change

* Remove package folder

* Undo change in ZipPluginPackage

* Add schama loader

* Clean up program.cs

* Add common options

* Add args validation and exception types

* Clean ups

* Add eof new lines and copy rights

* Remove .md files

* Remove unwanted files

* Remove generated xml

* Clean up constants.cs

* Use classes for each options instead of service locator

* Fix property name

* Separate options validation

* Decoupled commands and console

* Updated error handling

* Clean up

* Add manifest locators

* Address comments for utils method

* Move options validation to separate classes

* Address more comments

* Added base command

* Update protected to private

* Decorate with NotNullWhen

* Add docstrings

* Remove empty lines and fix warning

* Update package name and tool name

* Update helptext and add basic documentation

* Continue processing if BadImageFormatException is encountered during the scan

* Adrress more comments

* Use VersionChecker to access sdk versions

* Track all versions that have been checked and log version check errors

* Update logging based on pr comments
  • Loading branch information
helenkzhang authored Sep 6, 2023
1 parent 6330c1c commit ab49926
Show file tree
Hide file tree
Showing 47 changed files with 2,275 additions and 16 deletions.
28 changes: 28 additions & 0 deletions documentation/Plugins-System-Preview-Only/PluginTool-CLI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Plugintool CLI
The `plugintool` cli can be used to generate plugin metadata files (`metadata.json` and `metadatacontents.json`) and pack plugin binaries into `.ptix` packages.

### Usage
#### Metadata Files Generation

`plugintool metadata-gen [-s|--source <SOURCE_DIR>] [-o|--output <OUTPUT_DIR>] [-m|--manifest <MANIFEST_FILE>] [-w|--overwrite]`

- `-s|--source <SOURCE_DIR>` (Required)
Specifies the directory containing the plugin binaries.
- `-o|--output <OUTPUT_DIR>`
Specifies the directory where the metadata files will be created. If not provided, the files will be created in the current directory.
- `-m|--manifest <MANIFEST_FILE>`
Specifies the path to the manifest file. If not provided, the tool will look for a `pluginManifest.json` file in the source directory.
- `-w|--overwrite`
Specifies whether to overwrite existing metadata files if they exist. It's only valid if the `-o|--output` option is specified.

#### Package a Plugin
`plugintool pack [-s|--source <SOURCE_DIR>] [-o|--output <OUTPUT_FILE_PATH>] [-m|--manifest <MANIFEST_FILE>] [-w|--overwrite]` `

- `-s|--source <SOURCE_DIR>` (Required)
Specifies the directory containing the plugin binaries.
- `-o|--output <OUTPUT_FILE_PATH>`
Specifies the path where the `.ptix` package will be created. If not provided, the package will be created in the current directory.
- `-m|--manifest <MANIFEST_FILE>`
Specifies the path to the manifest file. If not provided, the tool will look for a `pluginManifest.json` file in the source directory.
- `-w|--overwrite`
Specifies whether to overwrite existing metadata files if they exist. It's only valid if the `-o|--output` option is specified.
54 changes: 54 additions & 0 deletions src/Microsoft.Performance.SDK.Runtime/TrackingVersionChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using System.Collections.Generic;
using NuGet.Versioning;

namespace Microsoft.Performance.SDK.Runtime
{
/// <summary>
/// A <see cref="VersionChecker"/> that keeps track of the versions it has checked and whether they are supported.
/// </summary>
public class TrackingVersionChecker
: VersionChecker
{
private readonly ConcurrentDictionary<SemanticVersion, bool> checkedVersion;

/// <summary>
/// Initializes a new instance of the <see cref="TrackingVersionChecker"/>
/// </summary>
public TrackingVersionChecker()
{
this.checkedVersion = new ConcurrentDictionary<SemanticVersion, bool>();
}

/// <summary>
/// Gets the list of versions that have been checked by this instance.
/// </summary>
public IReadOnlyDictionary<SemanticVersion, bool> CheckedVersions
{
get
{
return this.checkedVersion;
}
}

/// <summary>
/// Overrides the base implementation to keep track of the versions that have been checked.
/// </summary>
/// <param name="candidateVersion">
/// The version to check.
/// </param>
/// <returns>
/// <c>true</c> if the version is supported; <c>false</c> otherwise.
/// </returns>
public override bool IsVersionSupported(SemanticVersion candidateVersion)
{
bool supported = base.IsVersionSupported(candidateVersion);
this.checkedVersion.TryAdd(candidateVersion, supported);

return supported;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Performance.Toolkit.Plugins.Core.Metadata
{
/// <summary>
/// Contains the names of the non-file data sources that are supported by the toolkit
/// </summary>
public static class DataSourceNameConstants
{
public const string DirectoryDataSourceName = "directory";
public const string ExtensionlessDataSourceName = "extensionless";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public static class PackageConstants
public const string PluginMetadataFileName = "metadata.json";
public const string PluginContentsMetadataFileName = "contentsmetadata.json";
public const string PluginContentFolderName = "plugin/";
public const string PluginPackageExtension = ".ptix";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Performance.Toolkit.Plugins.Cli.Commands
{
/// <summary>
/// Common arguments for packgen commands.
/// </summary>
/// <param name="SourceDirectoryFullPath">
/// The full path to the directory containing the plugin.
/// </param>
/// <param name="ManifestFileFullPath">
/// The full path to the manifest file to use.
/// </param>
/// <param name="Overwrite">
/// Whether or not to overwrite an existing output file.
/// </param>
internal record PackGenCommonArgs(
string SourceDirectoryFullPath,
string? ManifestFileFullPath,
bool Overwrite);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Performance.Toolkit.Plugins.Cli.Manifest;
using Microsoft.Performance.Toolkit.Plugins.Cli.Processing;

namespace Microsoft.Performance.Toolkit.Plugins.Cli.Commands.Common
{
/// <summary>
/// Base class for commands that require the plugin to be processed.
/// </summary>
/// <typeparam name="TArgs">
/// The type of arguments for the command.
/// </typeparam>
internal abstract class PackGenCommonCommand<TArgs>
: ICommand<TArgs>
where TArgs : PackGenCommonArgs
{
protected readonly IManifestLocatorFactory manifestLocatorFactory;
protected readonly IPluginArtifactsProcessor artifactsProcessor;
protected readonly ILogger<PackGenCommonCommand<TArgs>> logger;

protected PackGenCommonCommand(
IManifestLocatorFactory manifestLocatorFactory,
IPluginArtifactsProcessor artifactsProcessor,
ILogger<PackGenCommonCommand<TArgs>> logger)
{
this.manifestLocatorFactory = manifestLocatorFactory;
this.artifactsProcessor = artifactsProcessor;
this.logger = logger;
}

/// <inheritdoc />
public int Run(TArgs args)
{
if (!TryGetProcessedPluginResult(args, out ProcessedPluginResult? processedSource))
{
return 1;
}

return RunCore(args, processedSource!);
}

protected abstract int RunCore(TArgs args, ProcessedPluginResult processedSource);

private bool TryGetProcessedPluginResult(PackGenCommonArgs args, [NotNullWhen(true)] out ProcessedPluginResult? processedPluginResult)
{
processedPluginResult = null;
IManifestLocator manifestLocator = this.manifestLocatorFactory.Create(args);
if (!manifestLocator.TryLocate(out string? manifestFilePath))
{
this.logger.LogError("Failed to locate manifest file.");
return false;
}

var artifacts = new PluginArtifacts(args.SourceDirectoryFullPath, manifestFilePath);
if (!this.artifactsProcessor.TryProcess(artifacts, out processedPluginResult))
{
this.logger.LogError("Failed to process plugin artifacts.");
return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Performance.Toolkit.Plugins.Cli.Console.Verbs;

namespace Microsoft.Performance.Toolkit.Plugins.Cli.Commands.Common
{
/// <summary>
/// Base class for validating common packgen options.
/// </summary>
/// <typeparam name="TOptions">
/// The type of options to validate.
/// </typeparam>
/// <typeparam name="TArgs">
/// The type of arguments to return.
/// </typeparam>
internal abstract class PackGenCommonOptionsValidator<TOptions, TArgs>
: IOptionsValidator<TOptions, TArgs>
where TOptions : PackGenCommonOptions
where TArgs : PackGenCommonArgs
{
protected readonly ILogger<PackGenCommonOptionsValidator<TOptions, TArgs>> logger;

protected PackGenCommonOptionsValidator(ILogger<PackGenCommonOptionsValidator<TOptions, TArgs>> logger)
{
this.logger = logger;
}

/// <inheritdoc />
public bool TryValidate(TOptions cliOptions, [NotNullWhen(true)] out TArgs? validatedAppArgs)
{
if (!TryValidateCommonOptions(cliOptions, out PackGenCommonArgs validatedCommonArgs))
{
validatedAppArgs = null;
return false;
}

return TryValidateCore(cliOptions, validatedCommonArgs, out validatedAppArgs);
}

protected abstract bool TryValidateCore(TOptions options, PackGenCommonArgs validatedCommonAppArgs, out TArgs? validatedAppArgs);

private bool TryValidateCommonOptions(
PackGenCommonOptions rawOptions,
out PackGenCommonArgs validatedAppArgs)
{
validatedAppArgs = null!;
if (string.IsNullOrWhiteSpace(rawOptions.SourceDirectory))
{
this.logger.LogError("Source directory must be specified. Use --source <path> or -s <path>.");
return false;
}

if (!Directory.Exists(rawOptions.SourceDirectory))
{
this.logger.LogError($"Source directory '{rawOptions.SourceDirectory}' does not exist.");
return false;
}

string sourceDirectoryFullPath = Path.GetFullPath(rawOptions.SourceDirectory);

// Validate manifest file path
string? manifestFileFullPath = null;
if (rawOptions.ManifestFilePath != null)
{
if (!File.Exists(rawOptions.ManifestFilePath))
{
this.logger.LogError($"Manifest file '{rawOptions.ManifestFilePath}' does not exist.");
return false;
}

manifestFileFullPath = Path.GetFullPath(rawOptions.ManifestFilePath);
}

validatedAppArgs = new PackGenCommonArgs(sourceDirectoryFullPath, manifestFileFullPath, rawOptions.Overwrite);
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Performance.Toolkit.Plugins.Cli.Commands
{
/// <summary>
/// Represents a command that can be executed via the CLI.
/// </summary>
/// <typeparam name="TArgs">
/// The type of arguments that the command accepts.
/// </typeparam>
internal interface ICommand<TArgs>
where TArgs : class
{
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="args">
/// The arguments to the command.
/// </param>
/// <returns>
/// The exit code of the command. A value of 0 indicates success. A value of 1 indicates failure.
/// </returns>
int Run(TArgs args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Performance.Toolkit.Plugins.Cli.Commands
{
/// <summary>
/// Represents the arguments for the <see cref="MetadataGenCommand"/>.
/// </summary>
/// <param name="SourceDirectoryFullPath">
/// The full path to the directory containing the source code for the plugin.
/// </param>
/// <param name="ManifestFileFullPath">
/// The full path to the manifest file for the plugin.
/// </param>
/// <param name="OutputDirectoryFullPath">
/// The full path to the directory to write the generated metadata to.
/// </param>
/// <param name="Overwrite">
/// Whether or not to overwrite existing files in the output directory.
/// </param>
internal record MetadataGenArgs(
string SourceDirectoryFullPath,
string? ManifestFileFullPath,
string? OutputDirectoryFullPath,
bool Overwrite)
: PackGenCommonArgs(
SourceDirectoryFullPath,
ManifestFileFullPath,
Overwrite)
{
/// <summary>
/// Initializes a new instance of the <see cref="MetadataGenArgs"/> class.
/// </summary>
/// <param name="commonArgs">
/// The common arguments.
/// </param>
/// <param name="outputDirectoryFullPath">
/// The full path to the directory to write the generated metadata.
/// </param>
public MetadataGenArgs(PackGenCommonArgs commonArgs, string? outputDirectoryFullPath)
: this(commonArgs.SourceDirectoryFullPath, commonArgs.ManifestFileFullPath, outputDirectoryFullPath, commonArgs.Overwrite)
{
}
}
}
Loading

0 comments on commit ab49926

Please sign in to comment.