diff --git a/src/SSHDebugPS/ConnectionManager.cs b/src/SSHDebugPS/ConnectionManager.cs index 0222926b0..3bd40dec3 100644 --- a/src/SSHDebugPS/ConnectionManager.cs +++ b/src/SSHDebugPS/ConnectionManager.cs @@ -38,7 +38,7 @@ public static DockerConnection GetDockerConnection(string name, bool supportSSHC { string connectionString; - bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, out connectionString); + bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, ContainerRuntimeType.Docker, out connectionString); if (success) { success = DockerConnection.TryConvertConnectionStringToSettings(connectionString, out settings, out remoteConnection); @@ -146,11 +146,12 @@ public static SSHConnection GetSSHConnection(string name) /// /// Parent hwnd or IntPtr.Zero /// SSHConnections are supported + /// Which container runtime to query /// [out] connection string obtained by the dialog - public static bool ShowContainerPickerWindow(IntPtr hwnd, bool supportSSHConnections, out string connectionString) + public static bool ShowContainerPickerWindow(IntPtr hwnd, bool supportSSHConnections, ContainerRuntimeType runtimeType, out string connectionString) { ThreadHelper.ThrowIfNotOnUIThread("Microsoft.SSHDebugPS.ShowContainerPickerWindow"); - ContainerPickerDialogWindow dialog = new ContainerPickerDialogWindow(supportSSHConnections); + ContainerPickerDialogWindow dialog = new ContainerPickerDialogWindow(supportSSHConnections, runtimeType); if (hwnd == IntPtr.Zero) // get the VS main window hwnd { diff --git a/src/SSHDebugPS/ContainerRuntimeType.cs b/src/SSHDebugPS/ContainerRuntimeType.cs new file mode 100644 index 000000000..1a8afa73a --- /dev/null +++ b/src/SSHDebugPS/ContainerRuntimeType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SSHDebugPS +{ + /// + /// Identifies the container runtime to query when discovering containers. + /// + public enum ContainerRuntimeType + { + Unknown, + Docker + } +} diff --git a/src/SSHDebugPS/ContainerTransportSettingsBase.cs b/src/SSHDebugPS/ContainerTransportSettingsBase.cs new file mode 100644 index 000000000..79462f4f9 --- /dev/null +++ b/src/SSHDebugPS/ContainerTransportSettingsBase.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SSHDebugPS.Utilities; +using System.Diagnostics; + +namespace Microsoft.SSHDebugPS +{ + internal abstract class ContainerTransportSettingsBase : IPipeTransportSettings + { + protected abstract string SubCommand { get; } + protected abstract string SubCommandArgs { get; } + + private readonly string _windowsExe; + private readonly string _unixExe; + private readonly string _hostnameFormat; + + internal string HostName { get; private set; } + internal bool HostIsUnix { get; private set; } + + public ContainerTransportSettingsBase(string hostname, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + { + HostIsUnix = hostIsUnix; + _windowsExe = windowsExe; + _unixExe = unixExe; + _hostnameFormat = hostnameFormat; + if (!string.IsNullOrWhiteSpace(hostname)) + { + HostName = hostname; + } + else + { + HostName = string.Empty; + } + } + + public ContainerTransportSettingsBase(ContainerTransportSettingsBase settings) + : this(settings.HostName, settings.HostIsUnix, settings._windowsExe, settings._unixExe, settings._hostnameFormat) + { } + + // 0 = command parameters (e.g. --host/--url) + // 1 = subcommand (e.g. exec, cp, ps) + // 2 = subcommand parameters + private const string _baseCommandFormat = "{0} {1} {2}"; + + private string GenerateExeCommandArgs() + { + var hostnameArg = string.Empty; + if (!string.IsNullOrWhiteSpace(this.HostName)) + hostnameArg = _hostnameFormat.FormatInvariantWithArgs(this.HostName); + + return _baseCommandFormat.FormatInvariantWithArgs(hostnameArg, SubCommand, SubCommandArgs); + } + + #region IPipeTransportSettings + + public string CommandArgs => GenerateExeCommandArgs(); + + public string Command => HostIsUnix ? _unixExe : _windowsExe; + + #endregion + } + + internal abstract class ContainerTargetTransportSettings : ContainerTransportSettingsBase + { + internal string ContainerName { get; private set; } + + public ContainerTargetTransportSettings(string hostname, string containerName, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { + ContainerName = containerName; + } + + public ContainerTargetTransportSettings(ContainerTargetTransportSettings settings) + : base(settings) + { + ContainerName = settings.ContainerName; + } + + protected override string SubCommand => throw new System.NotImplementedException(); + protected override string SubCommandArgs => throw new System.NotImplementedException(); + } + + internal abstract class ContainerExecSettings : ContainerTargetTransportSettings + { + private bool _runInShell; + private string _commandToExecute; + // 0 = container, 1 = command to execute + private const string _subCommandArgsFormat = "{0} {1}"; + private const string _subCommandArgsFormatWithShell = "{0} /bin/sh -c \"{1}\""; + private const string _subCommandArgsFormatWithShellLinuxHost = "{0} /bin/sh -c '{1}'"; + private const string _interactiveFlag = "-i "; + + private bool _makeInteractive; + + public ContainerExecSettings(ContainerTargetTransportSettings settings, string command, bool runInShell, bool makeInteractive = true) + : base(settings) + { + Debug.Assert(!string.IsNullOrWhiteSpace(command), "Exec command cannot be null"); + _runInShell = runInShell; + _commandToExecute = command; + _makeInteractive = makeInteractive; + } + + protected override string SubCommand => "exec"; + protected override string SubCommandArgs + { + get + { + string subCommandFormat = this.HostIsUnix ? _subCommandArgsFormatWithShellLinuxHost : _subCommandArgsFormatWithShell; + // Escape single quotes on Linux so variable resolution does not happen until it is in the container. + string command = this.HostIsUnix ? _commandToExecute.Replace("'", "'\\''") : _commandToExecute; + return (_makeInteractive ? _interactiveFlag : string.Empty) + + (_runInShell ? subCommandFormat : _subCommandArgsFormat).FormatInvariantWithArgs(ContainerName, command); + } + } + } + + internal abstract class ContainerCopySettings : ContainerTargetTransportSettings + { + // {0} = container, {1} = source, {2} = destination + private const string _copyFormatToContainer = "{1} {0}:{2}"; + + private string _sourcePath; + private string _destinationPath; + + public ContainerCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, containerName, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { + _sourcePath = sourcePath; + _destinationPath = destinationPath; + } + + public ContainerCopySettings(ContainerTargetTransportSettings settings, string sourcePath, string destinationPath) + : base(settings) + { + _sourcePath = sourcePath; + _destinationPath = destinationPath; + } + + protected override string SubCommand => "cp"; + protected override string SubCommandArgs => _copyFormatToContainer.FormatInvariantWithArgs(ContainerName, _sourcePath, _destinationPath); + } + + internal abstract class ContainerCommandSettings : ContainerTransportSettingsBase + { + private string _cmd; + private string _args; + + public ContainerCommandSettings(string hostname, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { } + + public void SetCommand(string cmd, string args) + { + _cmd = cmd; + _args = args; + } + + protected override string SubCommand => _cmd; + protected override string SubCommandArgs => _args; + } +} diff --git a/src/SSHDebugPS/Docker/DockerConnection.cs b/src/SSHDebugPS/Docker/DockerConnection.cs index d152e61ac..846245dbf 100644 --- a/src/SSHDebugPS/Docker/DockerConnection.cs +++ b/src/SSHDebugPS/Docker/DockerConnection.cs @@ -198,7 +198,7 @@ private ICommandRunner GetExecCommandRunner(string commandText, bool handleRawOu return GetCommandRunner(execSettings, handleRawOutput: handleRawOutput); } - private ICommandRunner GetCommandRunner(DockerContainerTransportSettings settings, bool handleRawOutput = false) + private ICommandRunner GetCommandRunner(IPipeTransportSettings settings, bool handleRawOutput = false) { if (OuterConnection == null) { diff --git a/src/SSHDebugPS/Docker/DockerContainerInstance.cs b/src/SSHDebugPS/Docker/DockerContainerInstance.cs index 810975773..e0df057a6 100644 --- a/src/SSHDebugPS/Docker/DockerContainerInstance.cs +++ b/src/SSHDebugPS/Docker/DockerContainerInstance.cs @@ -37,7 +37,7 @@ public static bool TryCreate(string json, out DockerContainerInstance instance) return instance != null; } - private DockerContainerInstance() { } + protected DockerContainerInstance() { } #region JsonProperties @@ -48,19 +48,19 @@ private DockerContainerInstance() { } public override string Name { get; set; } [JsonProperty(nameof(Image))] - public string Image { get; private set; } + public virtual string Image { get; protected set; } [JsonProperty(nameof(Ports))] - public string Ports { get; set; } + public virtual string Ports { get; set; } [JsonProperty(nameof(Command))] - public string Command { get; private set; } + public virtual string Command { get; protected set; } [JsonProperty(nameof(Status))] - public string Status { get; private set; } + public virtual string Status { get; protected set; } [JsonProperty("CreatedAt")] - public string Created { get; private set; } + public virtual string Created { get; protected set; } [JsonIgnore] public string Platform { get; set; } diff --git a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs new file mode 100644 index 000000000..d47aad0b6 --- /dev/null +++ b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.UI; + +namespace Microsoft.SSHDebugPS +{ + internal sealed class DockerDiscoveryStrategy : IContainerDiscoveryStrategy + { + private const string unknownOS = "Unknown"; + + public string ConnectionLabel => UIResources.ConnectionLabel; + public string HostnameLabel => UIResources.HostnameLabel; + public string HostnameTip => UIResources.HostnameTip; + public string ConnectionToolTip => UIResources.ConnectionToolTip; + public string HostnameAutomationName => UIResources.HostnameAutomationName; + + public IEnumerable GetLocalContainers(string hostname, out int totalContainers) + { + return DockerHelper.GetLocalDockerContainers(hostname, out totalContainers); + } + + public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers) + { + return DockerHelper.GetRemoteDockerContainers(connection, hostname, out totalContainers); + } + + public void AssignPlatforms(IEnumerable containers, string hostname) + { + if (!containers.Any()) + return; + + string serverOS; + if (DockerHelper.TryGetServerOS(hostname, out serverOS)) + { + bool lcow; + DockerHelper.TryGetLCOW(hostname, out lcow); + TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; + + if (lcow && serverOS.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0) + { + foreach (DockerContainerInstance container in containers) + { + string containerPlatform = string.Empty; + if (DockerHelper.TryGetContainerPlatform(hostname, container.Name, out containerPlatform)) + { + container.Platform = textInfo.ToTitleCase(containerPlatform); + } + else + { + container.Platform = unknownOS; + } + } + } + else + { + string platform = textInfo.ToTitleCase(serverOS); + foreach (DockerContainerInstance container in containers) + { + container.Platform = platform; + } + } + } + else + { + foreach (DockerContainerInstance container in containers) + { + container.Platform = unknownOS; + } + } + } + } +} diff --git a/src/SSHDebugPS/Docker/DockerExecutionManager.cs b/src/SSHDebugPS/Docker/DockerExecutionManager.cs index 4d36eb4cc..e2f018926 100644 --- a/src/SSHDebugPS/Docker/DockerExecutionManager.cs +++ b/src/SSHDebugPS/Docker/DockerExecutionManager.cs @@ -52,18 +52,27 @@ internal class DockerExecutionManager private PipeAsyncCommand _currentCommand; private Connection _outerConnection = null; - private DockerContainerTransportSettings _baseSettings; + private ContainerTargetTransportSettings _baseSettings; private readonly ManualResetEvent _commandCompleteEvent = new ManualResetEvent(false); - public DockerExecutionManager(DockerContainerTransportSettings baseSettings, Connection outerConnection) + public DockerExecutionManager(ContainerTargetTransportSettings baseSettings, Connection outerConnection) { _baseSettings = baseSettings; _outerConnection = outerConnection; } + protected virtual ContainerExecSettings CreateExecSettings(ContainerTargetTransportSettings baseSettings, string command, bool runInShell, bool makeInteractive) + { + if (!(baseSettings is DockerContainerTransportSettings dockerSettings)) + { + throw new ArgumentException($"Expected {nameof(DockerContainerTransportSettings)} but got {baseSettings.GetType().Name}", nameof(baseSettings)); + } + return new DockerExecSettings(dockerSettings, command, runInShell, makeInteractive); + } + private ICommandRunner GetExecCommandRunner(string command, bool runInShell, bool makeInteractive) { - var execSettings = new DockerExecSettings(_baseSettings, command, runInShell, makeInteractive); + var execSettings = CreateExecSettings(_baseSettings, command, runInShell, makeInteractive); if (_outerConnection == null) { diff --git a/src/SSHDebugPS/Docker/DockerHelper.cs b/src/SSHDebugPS/Docker/DockerHelper.cs index 5bbd7889c..f5a7a06ae 100644 --- a/src/SSHDebugPS/Docker/DockerHelper.cs +++ b/src/SSHDebugPS/Docker/DockerHelper.cs @@ -26,7 +26,7 @@ public class DockerHelper private const string dockerInspectArgs = "-f \"{{json .Platform}}\" "; private static char[] charsToTrim = { ' ', '\"' }; - private static void RunDockerCommand(DockerCommandSettings settings, Action callback) + internal static void RunContainerCommand(IPipeTransportSettings settings, Action callback) { LocalCommandRunner commandRunner = new LocalCommandRunner(settings); @@ -106,7 +106,7 @@ internal static bool TryGetLCOW(string hostname, out bool lcow) try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { if (args.Contains("lcow")) { @@ -134,7 +134,7 @@ internal static bool TryGetServerOS(string hostname, out string serverOS) try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { delegateServerOS = args; }); @@ -159,7 +159,7 @@ internal static bool TryGetContainerPlatform(string hostname, string containerNa try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { delegateContainerPlatform = args; }); @@ -183,7 +183,7 @@ internal static IEnumerable GetLocalDockerContainers(st DockerCommandSettings settings = new DockerCommandSettings(hostname, false); settings.SetCommand(dockerPSCommand, dockerPSArgs); - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { if (args.Trim()[0] == '{') { diff --git a/src/SSHDebugPS/Docker/DockerPortPicker.cs b/src/SSHDebugPS/Docker/DockerPortPicker.cs index d098bb7f3..4489e14c8 100644 --- a/src/SSHDebugPS/Docker/DockerPortPicker.cs +++ b/src/SSHDebugPS/Docker/DockerPortPicker.cs @@ -33,12 +33,13 @@ public class DockerWindowsPortPicker : DockerPortPickerBase public abstract class DockerPortPickerBase : IDebugPortPicker { internal abstract bool SupportSSHConnections { get; } + internal virtual ContainerRuntimeType RuntimeType => ContainerRuntimeType.Docker; int IDebugPortPicker.DisplayPortPicker(IntPtr hwndParentDialog, out string pbstrPortId) { ThreadHelper.ThrowIfNotOnUIThread(); // If this is null, then the PortPicker handler shows an error. Set to empty by default - return ConnectionManager.ShowContainerPickerWindow(hwndParentDialog, SupportSSHConnections, out pbstrPortId) ? + return ConnectionManager.ShowContainerPickerWindow(hwndParentDialog, SupportSSHConnections, RuntimeType, out pbstrPortId) ? VSConstants.S_OK : VSConstants.S_FALSE; } diff --git a/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs b/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs index 593eea60a..cf61e1c64 100644 --- a/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs +++ b/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs @@ -1,97 +1,38 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.SSHDebugPS.Utilities; -using System.Diagnostics; - namespace Microsoft.SSHDebugPS.Docker { - internal class DockerContainerTransportSettings : DockerTransportSettingsBase + internal sealed class DockerContainerTransportSettings : ContainerTargetTransportSettings { - internal string ContainerName { get; private set; } + internal const string WindowsExeName = "docker.exe"; + internal const string UnixExeName = "docker"; + internal const string HostFlag = "--host \"{0}\""; public DockerContainerTransportSettings(string hostname, string containerName, bool hostIsUnix) - : base(hostname, hostIsUnix) - { - ContainerName = containerName; - } + : base(hostname, containerName, hostIsUnix, WindowsExeName, UnixExeName, HostFlag) + { } public DockerContainerTransportSettings(DockerContainerTransportSettings settings) : base(settings) - { - ContainerName = settings.ContainerName; - } - - protected override string SubCommand => throw new System.NotImplementedException(); - protected override string SubCommandArgs => throw new System.NotImplementedException(); + { } } - internal class DockerExecSettings : DockerContainerTransportSettings + internal sealed class DockerExecSettings : ContainerExecSettings { - private bool _runInShell; - private string _commandToExecute; - // 0 = container, 1 = command to execute - private const string _subCommandArgsFormat = "{0} {1}"; - private const string _subCommandArgsFormatWithShell = "{0} /bin/sh -c \"{1}\""; - private const string _subCommandArgsFormatWithShellLinuxHost = "{0} /bin/sh -c '{1}'"; // Single quote the argument on Linux so variable resolution does not happen until it is in the container. - private const string _interactiveFlag = "-i "; - - private bool _makeInteractive; - public DockerExecSettings(DockerContainerTransportSettings settings, string command, bool runInShell, bool makeInteractive = true) - : base(settings) - { - Debug.Assert(!string.IsNullOrWhiteSpace(command), "Exec command cannot be null"); - _runInShell = runInShell; - _commandToExecute = command; - _makeInteractive = makeInteractive; - } - - protected override string SubCommand => "exec"; - protected override string SubCommandArgs - { - get - { - string subCommandFormat = this.HostIsUnix ? _subCommandArgsFormatWithShellLinuxHost : _subCommandArgsFormatWithShell; - // Because _subCommandArgsFormatWithShellLinuxHost single quotes the the subcommand arguments, we need to escape the command's single quotes - // by closing the single quotes and adding an escaped single quote and then reopening the single quote. - string command = this.HostIsUnix ? _commandToExecute.Replace("'", "'\\''") : _commandToExecute; - return (_makeInteractive ? _interactiveFlag : string.Empty) + - (_runInShell ? subCommandFormat : _subCommandArgsFormat).FormatInvariantWithArgs(ContainerName, command); - } - } + : base(settings, command, runInShell, makeInteractive) + { } } - internal class DockerCopySettings : DockerContainerTransportSettings + internal sealed class DockerCopySettings : ContainerCopySettings { - // {0} = container, {1} = source, {2} = destination - private string _copyFormatToContainer = "{1} {0}:{2}"; - - private string _sourcePath; - private string _destinationPath; - - /// - /// Settings to copy from host to the docker container - /// - /// Local path on host - /// Remote path within the docker container - /// Name of container - /// Host is Unix public DockerCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix) - : base(hostname, containerName, hostIsUnix) - { - _sourcePath = sourcePath; - _destinationPath = destinationPath; - } + : base(hostname, sourcePath, destinationPath, containerName, hostIsUnix, DockerContainerTransportSettings.WindowsExeName, DockerContainerTransportSettings.UnixExeName, DockerContainerTransportSettings.HostFlag) + { } public DockerCopySettings(DockerContainerTransportSettings settings, string sourcePath, string destinationPath) - : base(settings) - { - _sourcePath = sourcePath; - _destinationPath = destinationPath; - } - - protected override string SubCommand => "cp"; - protected override string SubCommandArgs => _copyFormatToContainer.FormatInvariantWithArgs(ContainerName, _sourcePath, _destinationPath); + : base(settings, sourcePath, destinationPath) + { } } } diff --git a/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs b/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs index 63e5f0c43..9b1024f85 100644 --- a/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs +++ b/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs @@ -1,78 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.SSHDebugPS.Utilities; - namespace Microsoft.SSHDebugPS.Docker { - internal abstract class DockerTransportSettingsBase : IPipeTransportSettings - { - protected abstract string SubCommand { get; } - protected abstract string SubCommandArgs { get; } - - internal string HostName { get; private set; } - internal bool HostIsUnix { get; private set; } - - public DockerTransportSettingsBase(string hostname, bool hostIsUnix) - { - HostIsUnix = hostIsUnix; - if (!string.IsNullOrWhiteSpace(hostname)) - { - HostName = hostname; - } - else - { - HostName = string.Empty; - } - } - - public DockerTransportSettingsBase(DockerTransportSettingsBase settings) - : this(settings.HostName, settings.HostIsUnix) - { } - - private static string WindowsExe => "docker.exe"; - private static string UnixExe => "docker"; - - // 0 = docker command parameters - // 1 = docker subcommand - // 2 = docker subcommand parameters - private const string _baseCommandFormat = "{0} {1} {2}"; - // 0 = hostname property - private const string _hostnameFormat = "--host \"{0}\""; - private string GenerateExeCommandArgs() - { - var hostnameArg = string.Empty; - if (!string.IsNullOrWhiteSpace(this.HostName)) - hostnameArg = _hostnameFormat.FormatInvariantWithArgs(this.HostName); - - return _baseCommandFormat.FormatInvariantWithArgs(hostnameArg, SubCommand, SubCommandArgs); - } - - #region IPipeTransportSettings - - public string CommandArgs => GenerateExeCommandArgs(); - - public string Command => HostIsUnix ? UnixExe : WindowsExe; - #endregion - } - - internal class DockerCommandSettings : DockerTransportSettingsBase + internal sealed class DockerCommandSettings : ContainerCommandSettings { - private string _cmd; - private string _args; - public DockerCommandSettings(string hostname, bool hostIsUnix) - : base(hostname, hostIsUnix) + : base(hostname, hostIsUnix, DockerContainerTransportSettings.WindowsExeName, DockerContainerTransportSettings.UnixExeName, DockerContainerTransportSettings.HostFlag) { } - - public void SetCommand(string cmd, string args) - { - _cmd = cmd; - _args = args; - } - - protected override string SubCommand => _cmd; - protected override string SubCommandArgs => _args; } } - diff --git a/src/SSHDebugPS/IContainerDiscoveryStrategy.cs b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs new file mode 100644 index 000000000..c7fd87bb7 --- /dev/null +++ b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.SSHDebugPS.Docker; + +namespace Microsoft.SSHDebugPS +{ + internal interface IContainerDiscoveryStrategy + { + string ConnectionLabel { get; } + string HostnameLabel { get; } + string HostnameTip { get; } + string ConnectionToolTip { get; } + string HostnameAutomationName { get; } + + IEnumerable GetLocalContainers(string hostname, out int totalContainers); + IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers); + void AssignPlatforms(IEnumerable containers, string hostname); + } +} diff --git a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml index cfdc1e8d6..81387168c 100644 --- a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml +++ b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml @@ -512,14 +512,14 @@ VerticalAlignment="Center" Style="{StaticResource MainLabelStyle}" Target="{Binding ElementName=ConnectionTypeComboBox}" - Content="{x:Static local:UIResources.ConnectionLabel}" /> + Content="{Binding ConnectionLabelText}" /> + Content="{Binding HostnameLabelText}" /> + ToolTip="{Binding HostnameTipText}"> diff --git a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs index feec9e5b7..ee0aae59c 100644 --- a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs +++ b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs @@ -25,9 +25,9 @@ namespace Microsoft.SSHDebugPS.UI /// public partial class ContainerPickerDialogWindow : DialogWindow { - public ContainerPickerDialogWindow(bool supportSSHConnections) + public ContainerPickerDialogWindow(bool supportSSHConnections, ContainerRuntimeType runtimeType) { - _model = new ContainerPickerViewModel(supportSSHConnections); + _model = new ContainerPickerViewModel(supportSSHConnections, runtimeType); this.DataContext = _model; this.Loaded += OnWindowLoaded; diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs index 2fefd0890..0f19c2f20 100644 --- a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs +++ b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs @@ -22,10 +22,11 @@ public class ContainerPickerViewModel : INotifyPropertyChanged { private Lazy _sshAvailable; - public ContainerPickerViewModel(bool supportSSHConnections) + public ContainerPickerViewModel(bool supportSSHConnections, ContainerRuntimeType runtimeType) { ThreadHelper.ThrowIfNotOnUIThread(); SupportSSHConnections = supportSSHConnections; + _discoveryStrategy = CreateDiscoveryStrategy(runtimeType); InitializeConnections(); ContainerInstances = new ObservableCollection(); @@ -142,7 +143,29 @@ private bool ComputeContainerConnectionString() // The formatted string for the ConnectionType dialog public string SelectedContainerConnectionString { get; private set; } - private const string unknownOS = "Unknown"; + private readonly IContainerDiscoveryStrategy _discoveryStrategy; + + private static IContainerDiscoveryStrategy CreateDiscoveryStrategy(ContainerRuntimeType runtimeType) + { + switch (runtimeType) + { + case ContainerRuntimeType.Docker: + return new DockerDiscoveryStrategy(); + default: + Debug.Fail($"Unsupported container runtime type: {runtimeType}"); + return null; + } + } + + public string ConnectionLabelText => _discoveryStrategy?.ConnectionLabel ?? UIResources.ConnectionLabel; + + public string HostnameLabelText => _discoveryStrategy?.HostnameLabel ?? UIResources.HostnameLabel; + + public string HostnameTipText => _discoveryStrategy?.HostnameTip ?? UIResources.HostnameTip; + + public string ConnectionToolTipText => _discoveryStrategy?.ConnectionToolTip ?? UIResources.ConnectionToolTip; + + public string HostnameAutomationNameText => _discoveryStrategy?.HostnameAutomationName ?? UIResources.HostnameAutomationName; private void RefreshContainersListInternal() { @@ -152,11 +175,19 @@ private void RefreshContainersListInternal() IContainerViewModel selectedContainer = SelectedContainerInstance; SelectedContainerInstance = null; + var viewModels = new List(); + + if (_discoveryStrategy == null) + { + UpdateStatusMessage(string.Format(CultureInfo.CurrentCulture, UIResources.ContainersFoundStatusText, 0), isError: true); + return; + } + IEnumerable containers; if (SelectedConnection is LocalConnectionViewModel) { - containers = DockerHelper.GetLocalDockerContainers(Hostname, out totalContainers); + containers = _discoveryStrategy.GetLocalContainers(Hostname, out totalContainers); } else { @@ -167,59 +198,16 @@ private void RefreshContainersListInternal() UpdateStatusMessage(UIResources.SSHConnectionFailedStatusText, isError: true); return; } - containers = DockerHelper.GetRemoteDockerContainers(connection, Hostname, out totalContainers); + containers = _discoveryStrategy.GetRemoteContainers(connection, Hostname, out totalContainers); } - if (containers.Any()) + if (containers != null) { - string serverOS; - - if (DockerHelper.TryGetServerOS(Hostname, out serverOS)) - { - bool lcow; - bool getLCOW = DockerHelper.TryGetLCOW(Hostname, out lcow); - TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; - serverOS = textInfo.ToTitleCase(serverOS); - - /* Note: LCOW is the abbreviation for Linux Containers on Windows - * - * In LCOW, both Linux and Windows containers can run simultaneously in a Docker (Windows) Engine. - * Thus, the container platform must be queried directly. - * Otherwise, the container platform must match that of the server engine. - */ - if (lcow && serverOS.Contains("Windows")) - { - foreach (DockerContainerInstance container in containers) - { - string containerPlatform = string.Empty; - if (DockerHelper.TryGetContainerPlatform(Hostname, container.Name, out containerPlatform)) - { - container.Platform = textInfo.ToTitleCase(containerPlatform); - } - else - { - container.Platform = unknownOS; - } - } - } - else - { - foreach (DockerContainerInstance container in containers) - { - container.Platform = serverOS; - } - } - } - else - { - foreach (DockerContainerInstance container in containers) - { - container.Platform = unknownOS; - } - } + _discoveryStrategy.AssignPlatforms(containers, Hostname); + viewModels.AddRange(containers.Select(item => (IContainerViewModel)new DockerContainerViewModel(item))); } - ContainerInstances = new ObservableCollection(containers.Select(item => new DockerContainerViewModel(item)).ToList()); + ContainerInstances = new ObservableCollection(viewModels); OnPropertyChanged(nameof(ContainerInstances)); if (ContainerInstances.Count > 0)