Core/src/MainUtil.cs (358 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using JetBrains.SymbolStorage.Impl;
using JetBrains.SymbolStorage.Impl.Commands;
using JetBrains.SymbolStorage.Impl.Logger;
using JetBrains.SymbolStorage.Impl.Storages;
using JetBrains.SymbolStorage.Impl.Tags;
using Microsoft.Extensions.CommandLineUtils;
namespace JetBrains.SymbolStorage
{
public static class MainUtil
{
public enum MainMode
{
Full,
UploadOnly
}
// Bug: Unix has one byte for exit code instead of Windows!!!
//
// Standard posix exit code meaning:
// 1 - Catchall for general errors
// 2 - Misuse of shell builtins (according to Bash documentation)
// 126 - Command invoked cannot execute
// 127 - “command not found”
// 128 - Invalid argument to exit
// 128+n - Fatal error signal “n”
// 130 - Script terminated by Control-C
// 255\* - Exit status out of range
public static byte Main(Assembly mainAssembly, string[] args, MainMode mode)
{
Stopwatch sw = Stopwatch.StartNew();
try
{
var assemblyName = mainAssembly.GetName();
var toolName = assemblyName.Name;
var toolVersion = assemblyName.Version!.ToString(3);
var commandLine = new CommandLineApplication
{
FullName = toolName
};
commandLine.HelpOption("-h|--help");
commandLine.VersionOption("--version", () => toolVersion);
var dirOption = commandLine.Option("-d|--directory", "The local directory with symbol server storage.", CommandOptionType.SingleValue);
var zipOption = commandLine.Option("--zip", "Zip file with symbol server storage.", CommandOptionType.SingleValue);
var awsS3BucketNameOption = commandLine.Option("-a|--aws-s3", $"The AWS S3 bucket with symbol server storage. The access and private keys will be asked in console. Use {AccessUtil.AwsS3AccessKeyEnvironmentVariable}, {AccessUtil.AwsS3SecretKeyEnvironmentVariable}, {AccessUtil.AwsS3SessionTokenEnvironmentVariable} (optional, '_' = no value) and {AccessUtil.AwsCloudFrontDistributionIdEnvironmentVariable} (optional, '_' = no value) environment variables for unattended mode.", CommandOptionType.SingleValue);
var awsS3RegionEndpointOption = commandLine.Option("-ar|--aws-s3-region", $"The AWS S3 region endpoint with symbol server storage. Default is {AccessUtil.DefaultAwsS3RegionEndpoint}.", CommandOptionType.SingleValue);
var degreeOfParallelismOption = commandLine.Option("-t|--tasks", $"Execute task count in parallel. Default is the processor count ({AccessUtil.DefaultDegreeOfParallelism} for now).", CommandOptionType.SingleValue);
var verboseOption = commandLine.Option("-v|--verbose", "Verbose mode.", CommandOptionType.NoValue);
static void FilterOptions(
CommandLineApplication x,
out CommandOption incFilterProductOption,
out CommandOption excFilterProductOption,
out CommandOption incFilterVersionOption,
out CommandOption excFilterVersionOption)
{
incFilterProductOption = x.Option("-fpi|--product-include-filter", "Select wildcard for include product filtering.", CommandOptionType.MultipleValue);
excFilterProductOption = x.Option("-fpe|--product-exclude-filter", "Select wildcard for exclude product filtering.", CommandOptionType.MultipleValue);
incFilterVersionOption = x.Option("-fvi|--version-include-filter", "Select wildcard for include version filtering.", CommandOptionType.MultipleValue);
excFilterVersionOption = x.Option("-fve|--version-exclude-filter", "Select wildcard for exclude version filtering.", CommandOptionType.MultipleValue);
}
if (mode == MainMode.Full)
{
commandLine.Command("validate", x =>
{
x.HelpOption("-h|--help");
x.Description = "Storage inconsistency check and fix known issues by request";
var aclOption = x.Option("-r|--rights", "Validate access rights.", CommandOptionType.NoValue);
var fixOption = x.Option("-f|--fix", "Fix known issues if possible.", CommandOptionType.NoValue);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
accessMode: fixOption.HasValue() ? AccessUtil.StorageAccessMode.ReadWrite : AccessUtil.StorageAccessMode.Read,
concurrencyLevel: degreeOfParallelism);
return await new ValidateCommand(
logger,
storage,
degreeOfParallelism,
aclOption.HasValue(),
fixOption.HasValue()).WithTimeReporting(logger).ExecuteAsync();
});
});
static void SafetyPeriodOptions(
CommandLineApplication x,
TimeSpan? defaultPeriod,
out CommandOption safetyPeriodOption)
{
string description = "The safety period for young files (files with a lower age will be skipped).";
if (defaultPeriod != null)
description += $" {defaultPeriod.Value.Days:D} days by default.";
safetyPeriodOption = x.Option("-sp|--safety-period", description, CommandOptionType.SingleValue);
}
commandLine.Command("list", x =>
{
x.HelpOption("-h|--help");
x.Description = "List storage metadata information";
FilterOptions(x,
out var incFilterProductOption,
out var excFilterProductOption,
out var incFilterVersionOption,
out var excFilterVersionOption);
SafetyPeriodOptions(x, null, out var safetyPeriodOption);
var filterProtectedOption = x.Option("-fr|--protected-filter", $"Filter by protected value: {AccessUtil.ProtectedAll}, {AccessUtil.ProtectedOn} and {AccessUtil.ProtectedOff}. The default is {AccessUtil.ProtectedAll}.", CommandOptionType.SingleValue);
var printFileSizesOption = x.Option("-sz|--file-sizes", "Print file sizes", CommandOptionType.NoValue);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.Read, degreeOfParallelism);
return await new ListCommand(
logger,
storage,
degreeOfParallelism,
new IdentityFilter(
incFilterProductOption.Values,
excFilterProductOption.Values,
incFilterVersionOption.Values,
excFilterVersionOption.Values),
ParseDays(safetyPeriodOption.Value(), defaultDays: null),
ParseProtected(filterProtectedOption.Value(), AccessUtil.ProtectedAll),
printFileSizesOption.HasValue()).WithTimeReporting(logger).ExecuteAsync();
});
});
commandLine.Command("delete", x =>
{
x.HelpOption("-h|--help");
x.Description = "Delete storage metadata and referenced data files";
FilterOptions(x,
out var incFilterProductOption,
out var excFilterProductOption,
out var incFilterVersionOption,
out var excFilterVersionOption);
SafetyPeriodOptions(x, AccessUtil.DefaultSafetyPeriod, out var safetyPeriodOption);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.ReadWrite, degreeOfParallelism);
return await new DeleteCommand(
logger,
storage,
degreeOfParallelism,
new IdentityFilter(
incFilterProductOption.Values,
excFilterProductOption.Values,
incFilterVersionOption.Values,
excFilterVersionOption.Values),
ParseDays(safetyPeriodOption.Value(), defaultDays: AccessUtil.DefaultSafetyPeriod).Value).WithTimeReporting(logger).ExecuteAsync();
});
});
}
static void StorageOptions(
CommandLineApplication x,
out CommandOption newStorageFormatOption)
{
newStorageFormatOption = x.Option("-nsf|--new-storage-format", $"Select data files format for a new storage: {AccessUtil.NormalStorageFormat} (default), {AccessUtil.LowerStorageFormat}, {AccessUtil.UpperStorageFormat}.", CommandOptionType.SingleValue);
}
commandLine.Command("new", x =>
{
x.HelpOption("-h|--help");
x.Description = "Create empty storage";
StorageOptions(x, out var newStorageFormatOption);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.ReadWrite, degreeOfParallelism);
return await new NewCommand(
logger,
storage,
AccessUtil.GetStorageFormat(newStorageFormatOption.Value())).WithTimeReporting(logger).ExecuteAsync();
});
});
commandLine.Command("upload", x =>
{
x.HelpOption("-h|--help");
x.Description = "Upload content from the source storage to another one with the source storage inconsistency check";
var sourceOption = x.Option("-s|--source", "Source storage directory or zip file.", CommandOptionType.SingleValue);
var collisionResolutionMode = x.Option("-crm|--collision-resolution", $"Collision resolution mode: {CollisionResolutionMode.Terminate} (default), {CollisionResolutionMode.KeepExisted}, {CollisionResolutionMode.Overwrite}, {CollisionResolutionMode.OverwriteWithoutBackup}.", CommandOptionType.SingleValue);
var peCollisionResolutionMode = x.Option("-crmpe|--collision-resolution-pe", $"Collision resolution mode override for PE weak hash: {CollisionResolutionMode.Terminate}, {CollisionResolutionMode.KeepExisted}, {CollisionResolutionMode.Overwrite}, {CollisionResolutionMode.OverwriteWithoutBackup}.", CommandOptionType.SingleValue);
var backupStorage = x.Option("-bckp|--backup-directory", "Directory to store backup in case of collisions", CommandOptionType.SingleValue);
StorageOptions(x, out var newStorageFormatOption);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var sourceStorage = AccessUtil.GetLocalFileSystemStorage(sourceOption.Value(), AccessUtil.StorageAccessMode.Read, degreeOfParallelism);
using var targetStorage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.ReadWrite, degreeOfParallelism);
return await new UploadCommand(
logger,
targetStorage,
degreeOfParallelism,
sourceStorage,
AccessUtil.GetStorageFormat(newStorageFormatOption.Value()),
collisionResolutionMode: AccessUtil.GetCollisionResolutionMode(collisionResolutionMode.Value()),
peCollisionResolutionMode: AccessUtil.GetCollisionResolutionMode(peCollisionResolutionMode.Value(), AccessUtil.GetCollisionResolutionMode(collisionResolutionMode.Value())),
backupStorageDir: backupStorage.Value()).WithTimeReporting(logger).ExecuteAsync();
});
});
commandLine.Command("create", x =>
{
x.HelpOption("-h|--help");
x.Description = "Create temporary storage and upload it to another one";
var compressWPdbOption = x.Option("-cwpdb|--compress-windows-pdb", "Enable compression for Windows PDB files. Windows only. Incompatible with the SSQP.", CommandOptionType.NoValue);
var compressPeOption = x.Option("-cpe|--compress-pe", "Enable compression for PE files. Windows only. Incompatible with the SSQP.", CommandOptionType.NoValue);
var keepNonCompressedOption = x.Option("-k|--keep-non-compressed", "Store also non-compressed version in storage.", CommandOptionType.NoValue);
var propertiesOption = x.Option("-p|--property", "The property to be stored in metadata in following format: <key1>=<value1>[,<key2>=<value2>[,...]]. Can be declared many times.", CommandOptionType.MultipleValue);
var protectedOption = x.Option("-r|--protected", "Protect files form deletion.", CommandOptionType.NoValue);
var collisionResolutionMode = x.Option("-crm|--collision-resolution", $"Collision resolution mode: {CollisionResolutionMode.Terminate} (default), {CollisionResolutionMode.KeepExisted}, {CollisionResolutionMode.Overwrite}, {CollisionResolutionMode.OverwriteWithoutBackup}.", CommandOptionType.SingleValue);
var peCollisionResolutionMode = x.Option("-crmpe|--collision-resolution-pe", $"Collision resolution mode override for PE weak hash: {CollisionResolutionMode.Terminate}, {CollisionResolutionMode.KeepExisted}, {CollisionResolutionMode.Overwrite}, {CollisionResolutionMode.OverwriteWithoutBackup}.", CommandOptionType.SingleValue);
var backupStorage = x.Option("-bckp|--backup-directory", "Directory to store backup in case of collisions", CommandOptionType.SingleValue);
StorageOptions(x, out var newStorageFormatOption);
var productArgument = x.Argument("product", "The product name.");
var versionArgument = x.Argument("version", "The product version.");
var sourcesOption = x.Argument("path [path [...]] or @file", "Source directories or files with symbols, executables and shared libraries.", true);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.ReadWrite, degreeOfParallelism);
var newStorageFormat = AccessUtil.GetStorageFormat(newStorageFormatOption.Value());
var sources = await ParsePaths(sourcesOption.Values);
var properties = propertiesOption.Values.ParseProperties();
var tempDir = Path.Combine(Path.GetTempPath(), "storage_" + Guid.NewGuid().ToString("D"));
var parsedCollisionResolutionMode = AccessUtil.GetCollisionResolutionMode(collisionResolutionMode.Value());
var parsedPeCollisionResolutionMode = AccessUtil.GetCollisionResolutionMode(peCollisionResolutionMode.Value(), parsedCollisionResolutionMode);
if ((parsedCollisionResolutionMode == CollisionResolutionMode.Overwrite || parsedPeCollisionResolutionMode == CollisionResolutionMode.Overwrite) && !backupStorage.HasValue())
throw new ArgumentException("Backup directory must be specified when collision resolution mode is 'overwrite'");
try
{
var res = await new CreateCommand(
logger,
new FileSystemStorage(tempDir),
degreeOfParallelism,
StorageFormat.Normal,
toolName + '/' + toolVersion,
new Identity(
productArgument.Value,
versionArgument.Value),
protectedOption.HasValue(),
compressPeOption.HasValue(),
compressWPdbOption.HasValue(),
keepNonCompressedOption.HasValue(),
properties,
sources).WithTimeReporting(logger).ExecuteAsync();
if (res != 0)
return res;
return await new UploadCommand(
logger,
storage,
degreeOfParallelism,
new FileSystemStorage(tempDir),
newStorageFormat,
parsedCollisionResolutionMode,
parsedPeCollisionResolutionMode,
backupStorage.Value()).WithTimeReporting(logger).ExecuteAsync();
}
finally
{
Directory.Delete(tempDir, true);
}
});
});
commandLine.Command("dump", x =>
{
x.HelpOption("-h|--help");
x.Description = "Dump symbol references";
var compressWPdbOption = x.Option("-cwpdb|--compress-windows-pdb", "Enable compression for Windows PDB files. Windows only. Incompatible with the SSQP.", CommandOptionType.NoValue);
var compressPeOption = x.Option("-cpe|--compress-pe", "Enable compression for PE files. Windows only. Incompatible with the SSQP.", CommandOptionType.NoValue);
var symbolReferenceFileOption = x.Argument("symref", "Symbol references file.");
var baseDirOption = x.Argument("basedir", "Base Directory.");
var sourcesOption = x.Argument("path [path [...]] or @file", "Source directories or files with symbols, executables and shared libraries.", true);
x.OnExecute(async () =>
{
var sources = await ParsePaths(sourcesOption.Values.Count != 0 ? sourcesOption.Values : new[] { baseDirOption.Value });
return await new DumpCommand(
new ConsoleLogger(verboseOption.HasValue()),
AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value()),
compressPeOption.HasValue(),
compressWPdbOption.HasValue(),
symbolReferenceFileOption.Value,
sources,
baseDirOption.Value).WithTimeReportingToConsole().ExecuteAsync();
});
});
commandLine.Command("protect", x =>
{
x.HelpOption("-h|--help");
x.Description = "Protect storage files from deletion";
FilterOptions(x,
out var incFilterProductOption,
out var excFilterProductOption,
out var incFilterVersionOption,
out var excFilterVersionOption);
var clearOption = x.Option("-c|--clear", "Clear protection.", CommandOptionType.NoValue);
x.OnExecute(async () =>
{
var logger = new ConsoleLogger(verboseOption.HasValue());
int degreeOfParallelism = AccessUtil.GetDegreeOfParallelism(degreeOfParallelismOption.Value());
using var storage = AccessUtil.GetStorage(dirOption.Value(), zipOption.Value(), awsS3BucketNameOption.Value(), awsS3RegionEndpointOption.Value(),
AccessUtil.StorageAccessMode.ReadWrite, degreeOfParallelism);
return await new ProtectedCommand(
new ConsoleLogger(verboseOption.HasValue()),
storage,
degreeOfParallelism,
new IdentityFilter(
incFilterProductOption.Values,
excFilterProductOption.Values,
incFilterVersionOption.Values,
excFilterVersionOption.Values),
!clearOption.HasValue()).WithTimeReporting(logger).ExecuteAsync();
});
});
if (args.Length != 0)
{
var res = commandLine.Execute(args);
if (0 <= res && res < 126)
return (byte)res;
return 255;
}
commandLine.ShowHint();
return 127;
}
catch (Exception e)
{
ConsoleLogger.Exception(e);
return 126;
}
finally
{
ConsoleLogger.WriteText($"Total execution time: {sw.Elapsed}");
}
}
[return: NotNullIfNotNull(nameof(defaultDays))]
private static TimeSpan? ParseDays(string? days, TimeSpan? defaultDays) => days != null ? TimeSpan.FromDays(ulong.Parse(days)) : defaultDays;
private static bool? ParseProtected(string? value, string defaultValue) => value != null
? AccessUtil.GetProtectedValue(value)
: AccessUtil.GetProtectedValue(defaultValue);
private static async Task<IReadOnlyCollection<string>> ParsePaths(IEnumerable<string> paths)
{
if (paths == null)
throw new ArgumentNullException(nameof(paths));
var res = new List<string>();
foreach (var path in paths)
if (path.StartsWith('@'))
{
using var reader = new StreamReader(path.Substring(1));
string? line;
while ((line = await reader.ReadLineAsync()) != null)
if (line.Length != 0)
res.Add(line);
}
else
res.Add(path);
return res;
}
}
}