diff --git a/Directory.Packages.props b/Directory.Packages.props index 1a9168125f..af214e6a67 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs index 718304a338..8da05f8bb1 100644 --- a/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs +++ b/ILSpy.AddIn.Shared/Commands/OpenCodeItemCommand.cs @@ -140,7 +140,7 @@ protected override async void OnExecute(object sender, EventArgs e) return; } - OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "/navigateTo:" + + OpenAssembliesInILSpy(new ILSpyParameters(validRefs.Select(r => r.AssemblyFile), "--navigateto:" + (symbol.OriginalDefinition ?? symbol).GetDocumentationCommentId())); } diff --git a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs index 195940b68b..14f3f9227f 100644 --- a/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs +++ b/ILSpy.AddIn.Shared/ILSpyAddInPackage.cs @@ -97,6 +97,24 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke OpenReferenceCommand.Register(this); OpenCodeItemCommand.Register(this); } + + protected override int QueryClose(out bool canClose) + { + var tempFiles = ILSpyInstance.TempFiles; + while (tempFiles.TryPop(out var filename)) + { + try + { + System.IO.File.Delete(filename); + } + catch (Exception) + { + } + } + + return base.QueryClose(out canClose); + } + #endregion public void ShowMessage(string format, params object[] items) diff --git a/ILSpy.AddIn.Shared/ILSpyInstance.cs b/ILSpy.AddIn.Shared/ILSpyInstance.cs index dc90e39a20..42059aa775 100644 --- a/ILSpy.AddIn.Shared/ILSpyInstance.cs +++ b/ILSpy.AddIn.Shared/ILSpyInstance.cs @@ -1,11 +1,9 @@ -using System; +using System.Collections.Concurrent; 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 { @@ -23,8 +21,9 @@ public ILSpyParameters(IEnumerable assemblyFileNames, params string[] ar class ILSpyInstance { - readonly ILSpyParameters parameters; + internal static readonly ConcurrentStack TempFiles = new ConcurrentStack(); + readonly ILSpyParameters parameters; public ILSpyInstance(ILSpyParameters parameters = null) { this.parameters = parameters; @@ -47,85 +46,30 @@ 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); - } - } + string assemblyArguments = string.Join("\r\n", commandLineArguments); - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "")] - 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 filepath = Path.GetTempFileName(); + File.WriteAllText(filepath, assemblyArguments); - // Wait some time before next attempt - await Task.Delay(500); - remainingAttempts--; - } while (!success && (remainingAttempts > 0)); - }); - } + TempFiles.Push(filepath); - 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(); } } } diff --git a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj index e0d1db28c2..83404ae2f7 100644 --- a/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj +++ b/ILSpy.AddIn.VS2022/ILSpy.AddIn.VS2022.csproj @@ -70,7 +70,6 @@ - diff --git a/ILSpy.AddIn/ILSpy.AddIn.csproj b/ILSpy.AddIn/ILSpy.AddIn.csproj index 5ad17dfb59..9b41766fd2 100644 --- a/ILSpy.AddIn/ILSpy.AddIn.csproj +++ b/ILSpy.AddIn/ILSpy.AddIn.csproj @@ -76,7 +76,6 @@ - diff --git a/ILSpy.Tests/CommandLineArgumentsTests.cs b/ILSpy.Tests/CommandLineArgumentsTests.cs new file mode 100644 index 0000000000..026c86b81a --- /dev/null +++ b/ILSpy.Tests/CommandLineArgumentsTests.cs @@ -0,0 +1,123 @@ +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 VerifyHelpOption() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "--help" }); + cmdLineArgs.ArgumentsParser.IsShowingInformation.Should().BeTrue(); + } + + [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 VerifyCaseSensitivityOfOptionsDoesntThrow() + { + var cmdLineArgs = new CommandLineArguments(new string[] { "--navigateTo:none" }); + + cmdLineArgs.ArgumentsParser.RemainingArguments.Should().HaveCount(1); + } + + [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 PassAtFileArguments() + { + string filepath = System.IO.Path.GetTempFileName(); + + System.IO.File.WriteAllText(filepath, "assembly1\r\nassembly2\r\nassembly3\r\n--newinstance\r\n--noactivate"); + + var cmdLineArgs = new CommandLineArguments(new string[] { $"@{filepath}" }); + + try + { + System.IO.File.Delete(filepath); + } + catch (Exception) + { + } + + cmdLineArgs.SingleInstance.Should().BeFalse(); + cmdLineArgs.NoActivate.Should().BeTrue(); + cmdLineArgs.AssembliesToLoad.Should().HaveCount(3); + } + } +} diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj index 79e95586a9..17337e7b24 100644 --- a/ILSpy.Tests/ILSpy.Tests.csproj +++ b/ILSpy.Tests/ILSpy.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 0e88f19caa..5779727c4b 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -87,6 +87,17 @@ public App() Hyperlink.RequestNavigateEvent, new RequestNavigateEventHandler(Window_RequestNavigate)); ILSpyTraceListener.Install(); + + if (App.CommandLineArguments.ArgumentsParser.IsShowingInformation) + { + MessageBox.Show(App.CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments"); + } + + if (App.CommandLineArguments.ArgumentsParser.RemainingArguments.Any()) + { + string unknownArguments = string.Join(", ", App.CommandLineArguments.ArgumentsParser.RemainingArguments); + MessageBox.Show(unknownArguments, "ILSpy Unknown Command Line Arguments Passed"); + } } static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) diff --git a/ILSpy/CommandLineArguments.cs b/ILSpy/CommandLineArguments.cs index c6c1b5947f..df1114e386 100644 --- a/ILSpy/CommandLineArguments.cs +++ b/ILSpy/CommandLineArguments.cs @@ -16,12 +16,14 @@ // 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 { - sealed class CommandLineArguments + public sealed class CommandLineArguments { // see /doc/Command Line.txt for details public List AssembliesToLoad = new List(); @@ -32,33 +34,70 @@ sealed class CommandLineArguments public bool NoActivate; public string ConfigFile; + public CommandLineApplication ArgumentsParser { get; } + public CommandLineArguments(IEnumerable 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.ParseArgsAsLineSeparated, + + // Note: options are case-sensitive (!), and, default behavior would be UnrecognizedArgumentHandling.Throw on Parse() + UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.CollectAndContinue + }; + + app.HelpOption(); + ArgumentsParser = app; + + 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("-n|--navigateto ", + "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("-s|--search ", + "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("-l|--language ", + "Selects the specified language.\r\nExample: 'ILSpy --language:C#' or 'ILSpy --language:IL'", + CommandOptionType.SingleValue); + oLanguage.DefaultValue = null; + + var oConfig = app.Option("-c|--config ", + "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); + + 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); } } } diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 93feab3a11..0ecb0caad8 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -45,6 +45,7 @@ + @@ -65,7 +66,6 @@ - diff --git a/ILSpy/Properties/launchSettings.json b/ILSpy/Properties/launchSettings.json index 706f4dfc0b..79731519a9 100644 --- a/ILSpy/Properties/launchSettings.json +++ b/ILSpy/Properties/launchSettings.json @@ -1,13 +1,13 @@ { - "profiles": { - "ILSpy": { - "commandName": "Executable", - "executablePath": ".\\ILSpy.exe", - "commandLineArgs": "/separate" - }, - "ILSpy single-instance": { - "commandName": "Executable", - "executablePath": ".\\ILSpy.exe" - } - } + "profiles": { + "ILSpy": { + "commandName": "Executable", + "executablePath": "./ilspy.exe", + "commandLineArgs": "--newinstance" + }, + "ILSpy single-instance": { + "commandName": "Executable", + "executablePath": "./ilspy.exe" + } + } } \ No newline at end of file diff --git a/ILSpy/SingleInstanceHandling.cs b/ILSpy/SingleInstanceHandling.cs index 5a59f1ee25..5736dffc74 100644 --- a/ILSpy/SingleInstanceHandling.cs +++ b/ILSpy/SingleInstanceHandling.cs @@ -64,10 +64,14 @@ internal static string FullyQualifyPath(string argument) { // Fully qualify the paths before passing them to another process, // because that process might use a different current directory. - if (string.IsNullOrEmpty(argument) || argument[0] == '/') + if (string.IsNullOrEmpty(argument) || argument[0] == '-') return argument; try { + if (argument.StartsWith("@")) + { + return "@" + FullyQualifyPath(argument.Substring(1)); + } return Path.Combine(Environment.CurrentDirectory, argument); } catch (ArgumentException) diff --git a/doc/Command Line.txt b/doc/Command Line.txt index 1cc041d66b..62a3a75512 100644 --- a/doc/Command Line.txt +++ b/doc/Command Line.txt @@ -1,54 +1,27 @@ ILSpy Command Line Arguments -Command line arguments can be either options or file names. -If an argument is a file name, the file will be opened as assembly and added to the current assembly list. +Usage: [options] + @ResponseFile.rsp -Available options: - /singleInstance If ILSpy is already running, activates the existing instance - and passes command line arguments to that instance. - This is the default value if /list is not used. - - /separate Start up a separate ILSpy instance even if it is already running. - - /noActivate Do not activate the existing ILSpy instance. This option has no effect - if a new ILSpy instance is being started. - - /list:listname Specifies the name of the assembly list that is loaded initially. - When this option is not specified, ILSpy loads the previously opened list. - Specify "/list" (without value) to open the default list. - - When this option is used, ILSpy will activate an existing instance - only if it uses the same list as specified. - - [Note: Assembly Lists are not yet implemented] - - /clearList Clears the assembly list before loading the specified assemblies. - [Note: Assembly Lists are not yet implemented] - - /navigateTo:tag Navigates to the member specified by the given ID string. - The member is searched for only in the assemblies specified on the command line. - Example: 'ILSpy ILSpy.exe /navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments' - - The syntax of ID strings is described in appendix A of the C# language specification. - - /language:name Selects the specified language. - Example: 'ILSpy /language:C#' or 'ILSpy /language:IL' +Arguments: + Assemblies Assemblies to load -WM_COPYDATA (SendMessage API): - ILSpy can be controlled by other programs that send a WM_COPYDATA message to its main window. - The message data must be an Unicode (UTF-16) string starting with "ILSpy:\r\n". - All lines except the first ("ILSpy:") in that string are handled as command-line arguments. - There must be exactly one argument per line. - - That is, by sending this message: - ILSpy: - C:\Assembly.dll - /navigateTo:T:Type - The target ILSpy instance will open C:\Assembly.dll and navigate to the specified type. - - ILSpy will return TRUE (1) if it handles the message, and FALSE (0) otherwise. - The /separate option will be ignored; WM_COPYDATA will never start up a new instance. - The /noActivate option has no effect, sending WM_COPYDATA will never activate the window. - Instead, the calling process should use SetForegroundWindow(). - If you use /list with WM_COPYDATA, you need to specify /singleInstance as well, otherwise - ILSpy will not handle the message if it has opened a different assembly list. +Options: + --newinstance Start a new instance of ILSpy even if the user configuration is set to single-instance + -n|--navigateto Navigates to the member specified by the given ID string. + The member is searched for only in the assemblies specified on the command line. + Example: 'ILSpy ILSpy.exe --navigateTo:T:ICSharpCode.ILSpy.CommandLineArguments' + -s|--search 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)?/... + -l|--language Selects the specified language. + Example: 'ILSpy --language:C#' or 'ILSpy --language:IL' + -c|--config Provide a specific configuration file. + Example: 'ILSpy --config:myconfig.xml' + --noactivate Do not activate the existing ILSpy instance. + This option has no effect if a new ILSpy instance is being started. + +Note on @ResponseFile.rsp: + +* The response file should contain the arguments, one argument per line (not space-separated!). +* Use it when the list of assemblies is too long to fit on the command line. \ No newline at end of file diff --git a/publishlocaldev.ps1 b/publishlocaldev.ps1 new file mode 100644 index 0000000000..a8ebda02fd --- /dev/null +++ b/publishlocaldev.ps1 @@ -0,0 +1,13 @@ +# For local development of the VSIX package - build and publish (VS2022 also needs arm64) + +$output_x64 = "./ILSpy/bin/Debug/net8.0-windows/win-x64/publish/fwdependent" + +dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 +dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 +dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-x64 -o $output_x64 + +$output_arm64 = "./ILSpy/bin/Debug/net8.0-windows/win-arm64/publish/fwdependent" + +dotnet publish ./ILSpy/ILSpy.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 +dotnet publish ./ILSpy.ReadyToRun/ILSpy.ReadyToRun.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 +dotnet publish ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj -c Debug --no-restore --no-self-contained -r win-arm64 -o $output_arm64 \ No newline at end of file