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

@file support with breaking changes to command line options #3205

Merged
merged 16 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageVersion Include="Iced" Version="1.21.0" />
<PackageVersion Include="JunitXml.TestLogger" Version="3.1.12" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.9.2" />
Expand Down
92 changes: 16 additions & 76 deletions ILSpy.AddIn.Shared/ILSpyInstance.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ICSharpCode.ILSpy.AddIn
{
Expand Down Expand Up @@ -47,85 +44,28 @@ public void Start()
{
var commandLineArguments = parameters?.AssemblyFileNames?.Concat(parameters.Arguments);
string ilSpyExe = GetILSpyPath();
var process = new Process() {
StartInfo = new ProcessStartInfo() {
FileName = ilSpyExe,
UseShellExecute = false,
Arguments = "/navigateTo:none"
}
};
process.Start();

const string defaultOptions = "--navigateto:none";
string argumentsToPass = defaultOptions;

if ((commandLineArguments != null) && commandLineArguments.Any())
{
// Only need a message to started process if there are any parameters to pass
SendMessage(ilSpyExe, "ILSpy:\r\n" + string.Join(Environment.NewLine, commandLineArguments), true);
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "<Pending>")]
void SendMessage(string ilSpyExe, string message, bool activate)
{
string expectedProcessName = Path.GetFileNameWithoutExtension(ilSpyExe);
// We wait asynchronously until target window can be found and try to find it multiple times
Task.Run(async () => {
bool success = false;
int remainingAttempts = 20;
do
{
NativeMethods.EnumWindows(
(hWnd, lParam) => {
string windowTitle = NativeMethods.GetWindowText(hWnd, 100);
if (windowTitle.StartsWith("ILSpy", StringComparison.Ordinal))
{
string processName = NativeMethods.GetProcessNameFromWindow(hWnd);
Debug.WriteLine("Found {0:x4}: '{1}' in '{2}'", hWnd, windowTitle, processName);
if (string.Equals(processName, expectedProcessName, StringComparison.OrdinalIgnoreCase))
{
IntPtr result = Send(hWnd, message);
Debug.WriteLine("WM_COPYDATA result: {0:x8}", result);
if (result == (IntPtr)1)
{
if (activate)
NativeMethods.SetForegroundWindow(hWnd);
success = true;
return false; // stop enumeration
}
}
}
return true; // continue enumeration
}, IntPtr.Zero);
string assemblyArguments = string.Join(" ", commandLineArguments);

// Wait some time before next attempt
await Task.Delay(500);
remainingAttempts--;
} while (!success && (remainingAttempts > 0));
});
}
string filepath = Path.GetTempFileName();
File.WriteAllText(filepath, assemblyArguments);
christophwille marked this conversation as resolved.
Show resolved Hide resolved

unsafe static IntPtr Send(IntPtr hWnd, string message)
{
const uint SMTO_NORMAL = 0;
argumentsToPass = $"@\"{filepath}\"";
}

CopyDataStruct lParam;
lParam.Padding = IntPtr.Zero;
lParam.Size = message.Length * 2;
fixed (char* buffer = message)
{
lParam.Buffer = (IntPtr)buffer;
IntPtr result;
// SendMessage with 3s timeout (e.g. when the target process is stopped in the debugger)
if (NativeMethods.SendMessageTimeout(
hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref lParam,
SMTO_NORMAL, 3000, out result) != IntPtr.Zero)
{
return result;
}
else
{
return IntPtr.Zero;
var process = new Process() {
StartInfo = new ProcessStartInfo() {
FileName = ilSpyExe,
UseShellExecute = false,
Arguments = argumentsToPass
}
}
};
process.Start();
}
}
}
1 change: 0 additions & 1 deletion ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
<Compile Include="..\ICSharpCode.Decompiler\Metadata\LightJson\Serialization\TextScanner.cs" Link="Decompiler\LightJson\Serialization\TextScanner.cs" />
<Compile Include="..\ICSharpCode.Decompiler\Metadata\UniversalAssemblyResolver.cs" Link="UniversalAssemblyResolver.cs" />
<Compile Include="..\ICSharpCode.Decompiler\Util\EmptyList.cs" Link="Decompiler\EmptyList.cs" />
<Compile Include="..\ILSpy\NativeMethods.cs" Link="NativeMethods.cs" />
<Compile Include="Decompiler\Dummy.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion ILSpy.AddIn/ILSpy.AddIn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
<Compile Include="..\ICSharpCode.Decompiler\Metadata\LightJson\Serialization\TextScanner.cs" Link="Decompiler\LightJson\Serialization\TextScanner.cs" />
<Compile Include="..\ICSharpCode.Decompiler\Metadata\UniversalAssemblyResolver.cs" Link="UniversalAssemblyResolver.cs" />
<Compile Include="..\ICSharpCode.Decompiler\Util\EmptyList.cs" Link="Decompiler\EmptyList.cs" />
<Compile Include="..\ILSpy\NativeMethods.cs" Link="NativeMethods.cs" />
<Compile Include="Decompiler\Dummy.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
Expand Down
109 changes: 109 additions & 0 deletions ILSpy.Tests/CommandLineArgumentsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;

using FluentAssertions;

using NUnit.Framework;

namespace ICSharpCode.ILSpy.Tests
{
[TestFixture]
public class CommandLineArgumentsTests
{
[Test]
public void VerifyEmptyArgumentsArray()
{
var cmdLineArgs = new CommandLineArguments(new string[] { });

cmdLineArgs.AssembliesToLoad.Should().BeEmpty();
cmdLineArgs.SingleInstance.Should().BeNull();
cmdLineArgs.NavigateTo.Should().BeNull();
cmdLineArgs.Search.Should().BeNull();
cmdLineArgs.Language.Should().BeNull();
cmdLineArgs.NoActivate.Should().BeFalse();
cmdLineArgs.ConfigFile.Should().BeNull();
}

[Test]
public void VerifyForceNewInstanceOption()
{
var cmdLineArgs = new CommandLineArguments(new string[] { "--newinstance" });
cmdLineArgs.SingleInstance.Should().BeFalse();
}

[Test]
public void VerifyNavigateToOption()
{
const string navigateTo = "MyNamespace.MyClass";
var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto", navigateTo });
cmdLineArgs.NavigateTo.Should().BeEquivalentTo(navigateTo);
}

[Test]
public void VerifyNavigateToOption_NoneTest_Matching_VSAddin()
{
var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateto:none" });
cmdLineArgs.NavigateTo.Should().BeEquivalentTo("none");
}

[Test]
public void VerifyCaseSensitivityOfOptionsThrows()
{
Action act = () => new CommandLineArguments(new string[] { "--navigateTo:none" });

act.Should().Throw<McMaster.Extensions.CommandLineUtils.UnrecognizedCommandParsingException>()
.WithMessage("Unrecognized option '--navigateTo:none'");
}

[Test]
public void VerifySearchOption()
{
const string searchWord = "TestContainers";
var cmdLineArgs = new CommandLineArguments(new string[] { "--search", searchWord });
cmdLineArgs.Search.Should().BeEquivalentTo(searchWord);
}

[Test]
public void VerifyLanguageOption()
{
const string language = "csharp";
var cmdLineArgs = new CommandLineArguments(new string[] { "--language", language });
cmdLineArgs.Language.Should().BeEquivalentTo(language);
}

[Test]
public void VerifyConfigOption()
{
const string configFile = "myilspyoptions.xml";
var cmdLineArgs = new CommandLineArguments(new string[] { "--config", configFile });
cmdLineArgs.ConfigFile.Should().BeEquivalentTo(configFile);
}

[Test]
public void VerifyNoActivateOption()
{
var cmdLineArgs = new CommandLineArguments(new string[] { "--noactivate" });
cmdLineArgs.NoActivate.Should().BeTrue();
}

[Test]
public void MultipleAssembliesAsArguments()
{
var cmdLineArgs = new CommandLineArguments(new string[] { "assembly1", "assembly2", "assembly3" });
cmdLineArgs.AssembliesToLoad.Should().HaveCount(3);
}

[Test]
public void PassAtFileArgumentsSpaceSeparated()
{
string filepath = System.IO.Path.GetTempFileName();

System.IO.File.WriteAllText(filepath, "assembly1 assembly2 assembly3 --newinstance --noactivate");

var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" });

cmdLineArgs.SingleInstance.Should().BeFalse();
cmdLineArgs.NoActivate.Should().BeTrue();
cmdLineArgs.AssembliesToLoad.Should().HaveCount(3);
}
}
}
1 change: 1 addition & 0 deletions ILSpy.Tests/ILSpy.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="Analyzers\MethodUsesAnalyzerTests.cs" />
<Compile Include="Analyzers\TestCases\MainAssembly.cs" />
<Compile Include="Analyzers\TypeUsedByAnalyzerTests.cs" />
<Compile Include="CommandLineArgumentsTests.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
85 changes: 60 additions & 25 deletions ILSpy/CommandLineArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using McMaster.Extensions.CommandLineUtils;

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

namespace ICSharpCode.ILSpy
{
Expand All @@ -34,31 +36,64 @@ sealed class CommandLineArguments

public CommandLineArguments(IEnumerable<string> arguments)
{
foreach (string arg in arguments)
var app = new CommandLineApplication() {
// https://natemcmaster.github.io/CommandLineUtils/docs/response-file-parsing.html?tabs=using-attributes
ResponseFileHandling = ResponseFileHandling.ParseArgsAsSpaceSeparated,

// Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse()
// UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue
};

var oForceNewInstance = app.Option("--newinstance",
"Start a new instance of ILSpy even if the user configuration is set to single-instance",
CommandOptionType.NoValue);

var oNavigateTo = app.Option<string>("-n|--navigateto <TYPENAME>",
"Navigates to the member specified by the given ID string.\r\nThe member is searched for only in the assemblies specified on the command line.\r\nExample: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments'",
CommandOptionType.SingleValue);
oNavigateTo.DefaultValue = null;

var oSearch = app.Option<string>("-s|--search <SEARCHTERM>",
"Search for t:TypeName, m:Member or c:Constant; use exact match (=term), 'should not contain' (-term) or 'must contain' (+term); use /reg(ular)?Ex(pressions)?/ or both - t:/Type(Name)?/...",
CommandOptionType.SingleValue);
oSearch.DefaultValue = null;

var oLanguage = app.Option<string>("-l|--language <LANGUAGEIDENTIFIER>",
"Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'",
CommandOptionType.SingleValue);
oLanguage.DefaultValue = null;

var oConfig = app.Option<string>("-c|--config <CONFIGFILENAME>",
"Provide a specific configuration file.\r\nExample: 'ILSpy --config:myconfig.xml'",
CommandOptionType.SingleValue);
oConfig.DefaultValue = null;

var oNoActivate = app.Option("--noactivate",
"Do not activate the existing ILSpy instance. This option has no effect if a new ILSpy instance is being started.",
CommandOptionType.NoValue);

// https://natemcmaster.github.io/CommandLineUtils/docs/arguments.html#variable-numbers-of-arguments
// To enable this, MultipleValues must be set to true, and the argument must be the last one specified.
var files = app.Argument("Assemblies", "Assemblies to load", multipleValues: true);

// string helptext = app.GetHelpText();
app.Parse(arguments.ToArray());

if (oForceNewInstance.HasValue())
SingleInstance = false;

NavigateTo = oNavigateTo.ParsedValue;
Search = oSearch.ParsedValue;
Language = oLanguage.ParsedValue;
ConfigFile = oConfig.ParsedValue;

if (oNoActivate.HasValue())
NoActivate = true;

foreach (var assembly in files.Values)
{
if (arg.Length == 0)
continue;
if (arg[0] == '/')
{
if (arg.Equals("/singleInstance", StringComparison.OrdinalIgnoreCase))
this.SingleInstance = true;
else if (arg.Equals("/separate", StringComparison.OrdinalIgnoreCase))
this.SingleInstance = false;
else if (arg.StartsWith("/navigateTo:", StringComparison.OrdinalIgnoreCase))
this.NavigateTo = arg.Substring("/navigateTo:".Length);
else if (arg.StartsWith("/search:", StringComparison.OrdinalIgnoreCase))
this.Search = arg.Substring("/search:".Length);
else if (arg.StartsWith("/language:", StringComparison.OrdinalIgnoreCase))
this.Language = arg.Substring("/language:".Length);
else if (arg.Equals("/noActivate", StringComparison.OrdinalIgnoreCase))
this.NoActivate = true;
else if (arg.StartsWith("/config:", StringComparison.OrdinalIgnoreCase))
this.ConfigFile = arg.Substring("/config:".Length);
}
else
{
this.AssembliesToLoad.Add(arg);
}
if (!string.IsNullOrWhiteSpace(assembly))
AssembliesToLoad.Add(assembly);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions ILSpy/ILSpy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<ItemGroup>
<PackageReference Include="AvalonEdit" />
<PackageReference Include="Dirkster.AvalonDock.Themes.VS2013" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" />
<PackageReference Include="Microsoft.VisualStudio.Composition" />
<PackageReference Include="DataGridExtensions" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
Expand Down
Loading
Loading