diff --git a/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef b/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef
index ce626ec28..85c7fe331 100644
--- a/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef
+++ b/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef
@@ -54,6 +54,8 @@
"1"="{A2BBC114-47E4-473F-A49C-69EE89711243}"
; WSL Port supplier
"2"="{267B1341-AC92-44DC-94DF-2EE4205DD17E}"
+; Podman Port Supplier
+"3"="{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}"
; Registration to use lldb with the port suppliers
[$RootKey$\AD7Metrics\Engine\{5D630903-189D-4837-9785-699B05BEC2A9}]
@@ -83,6 +85,8 @@
"0"="{3FDDF14E-E758-4695-BE0C-7509920432C9}"
; WSL Port supplier
"1"="{267B1341-AC92-44DC-94DF-2EE4205DD17E}"
+; Podman Port Supplier
+"2"="{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}"
[$RootKey$\AD7Metrics\Engine\{5D630903-189D-4837-9785-699B05BEC2A9}\IncompatibleList]
"MI Debug Engine - gdb"="{91744D97-430F-42C1-9779-A5813EBD6AB2}"
diff --git a/src/SSHDebugPS/ConnectionManager.cs b/src/SSHDebugPS/ConnectionManager.cs
index 0222926b0..bc8be3627 100644
--- a/src/SSHDebugPS/ConnectionManager.cs
+++ b/src/SSHDebugPS/ConnectionManager.cs
@@ -14,6 +14,7 @@
using liblinux;
using liblinux.Persistence;
using Microsoft.SSHDebugPS.Docker;
+using Microsoft.SSHDebugPS.Podman;
using Microsoft.SSHDebugPS.SSH;
using Microsoft.SSHDebugPS.UI;
using Microsoft.SSHDebugPS.Utilities;
@@ -38,7 +39,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);
@@ -65,6 +66,46 @@ public static DockerConnection GetDockerConnection(string name, bool supportSSHC
}
}
+ public static PodmanConnection GetPodmanConnection(string name, bool supportSSHConnections)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return null;
+
+ PodmanContainerTransportSettings settings;
+ Connection remoteConnection;
+
+ ThreadHelper.ThrowIfNotOnUIThread();
+ if (!PodmanConnection.TryConvertConnectionStringToSettings(name, out settings, out remoteConnection) || settings == null)
+ {
+ string connectionString;
+
+ bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, ContainerRuntimeType.Podman, out connectionString);
+ if (success)
+ {
+ success = PodmanConnection.TryConvertConnectionStringToSettings(connectionString, out settings, out remoteConnection);
+ }
+
+ if (!success || settings == null)
+ {
+ VSMessageBoxHelper.PostErrorMessage(StringResources.Error_ContainerConnectionStringInvalidTitle, StringResources.Error_ContainerConnectionStringInvalidMessage);
+ return null;
+ }
+ }
+
+ string displayName = PodmanConnection.CreateConnectionString(settings.ContainerName, remoteConnection?.Name, settings.HostName);
+ if (PodmanHelper.IsContainerRunning(settings.HostName, settings.ContainerName, remoteConnection))
+ {
+ return new PodmanConnection(settings, remoteConnection, displayName);
+ }
+ else
+ {
+ VSMessageBoxHelper.PostErrorMessage(
+ StringResources.Error_ContainerUnavailableTitle,
+ StringResources.Error_ContainerUnavailableMessage.FormatCurrentCultureWithArgs(settings.ContainerName));
+ return null;
+ }
+ }
+
public static SSHConnection GetSSHConnection(string name)
{
ThreadHelper.ThrowIfNotOnUIThread();
@@ -146,11 +187,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..612c91035
--- /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
+ {
+ Docker,
+ Podman
+ }
+}
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/ContainerInstance.cs b/src/SSHDebugPS/Docker/ContainerInstance.cs
new file mode 100644
index 000000000..1e830d4cb
--- /dev/null
+++ b/src/SSHDebugPS/Docker/ContainerInstance.cs
@@ -0,0 +1,137 @@
+// 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.Diagnostics;
+using Microsoft.DebugEngineHost;
+using Microsoft.SSHDebugPS.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SSHDebugPS.Docker
+{
+ public class ContainerInstance : IContainerInstance
+ {
+ ///
+ /// Create a ContainerInstance from the results of docker ps in JSON format
+ ///
+ public static bool TryCreate(string json, out ContainerInstance instance)
+ {
+ instance = null;
+ try
+ {
+ JObject obj = JObject.Parse(json);
+ instance = obj.ToObject();
+ }
+ catch (Exception e)
+ {
+ HostTelemetry.SendEvent(TelemetryHelper.Event_DockerPSParseFailure, new KeyValuePair[] {
+ new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name)
+ });
+
+ string error = e.ToString();
+ VsOutputWindowWrapper.WriteLine(StringResources.Error_DockerPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Docker_PSName);
+ Debug.Fail(error);
+ }
+ return instance != null;
+ }
+
+ protected ContainerInstance() { }
+
+ #region JsonProperties
+
+ [JsonProperty("ID")]
+ public virtual string Id { get; set; }
+
+ [JsonProperty("Names")]
+ public virtual string Name { get; set; }
+
+ [JsonProperty(nameof(Image))]
+ public virtual string Image { get; protected set; }
+
+ [JsonProperty(nameof(Ports))]
+ public virtual string Ports { get; set; }
+
+ [JsonProperty(nameof(Command))]
+ public virtual string Command { get; protected set; }
+
+ [JsonProperty(nameof(Status))]
+ public virtual string Status { get; protected set; }
+
+ [JsonProperty("CreatedAt")]
+ public virtual string Created { get; protected set; }
+
+ [JsonIgnore]
+ public string Platform { get; set; }
+
+ #endregion
+
+ #region IEquatable
+
+ public static bool operator ==(ContainerInstance left, ContainerInstance right)
+ {
+ if (left is null || right is null)
+ {
+ return ReferenceEquals(left, right);
+ }
+
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(ContainerInstance left, ContainerInstance right)
+ {
+ return !(left == right);
+ }
+
+ public bool Equals(IContainerInstance instance)
+ {
+ if (instance is ContainerInstance container)
+ {
+ return this.EqualsInternal(container);
+ }
+
+ return false;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is IContainerInstance instance)
+ {
+ return this.Equals(instance);
+ }
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return GetHashCodeInternal();
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ // Container names: only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed. It is also case sensitive
+ protected virtual bool EqualsInternal(ContainerInstance instance)
+ {
+ if (GetType() != instance.GetType())
+ {
+ return false;
+ }
+
+ // the id can be a partial on a container
+ return String.Equals(Id, instance.Id, StringComparison.Ordinal) ||
+ Id.StartsWith(instance.Id, StringComparison.Ordinal) ||
+ instance.Id.StartsWith(Id, StringComparison.Ordinal);
+ }
+
+ protected virtual int GetHashCodeInternal()
+ {
+ // Since IDs can be partial, we don't have a good way to get a good hash code.
+ return string.IsNullOrWhiteSpace(Id) ? 0 : Id.Substring(0,1).GetHashCode();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/SSHDebugPS/Docker/DockerConnection.cs b/src/SSHDebugPS/Docker/DockerConnection.cs
index d152e61ac..bf99504fb 100644
--- a/src/SSHDebugPS/Docker/DockerConnection.cs
+++ b/src/SSHDebugPS/Docker/DockerConnection.cs
@@ -20,8 +20,8 @@ internal class DockerConnection : PipeConnection
internal const string SshPrefixRegex = @"^[Ss]{2}[Hh]\s*=\s*";
internal const string SshPrefix = "ssh=";
- internal const string DockerHostPrefixRegex = @"^host\s*=\s*";
- internal const string DockerHostPrefix = "host=";
+ internal const string HostPrefixRegex = @"^host\s*=\s*";
+ internal const string HostPrefix = "host=";
internal const char Separator = ';';
internal static string CreateConnectionString(string containerName, string remoteConnectionName, string hostName)
@@ -34,7 +34,7 @@ internal static string CreateConnectionString(string containerName, string remot
if (!string.IsNullOrWhiteSpace(hostName))
{
- connectionString += Separator + DockerHostPrefix + hostName;
+ connectionString += Separator + HostPrefix + hostName;
}
return connectionString;
@@ -56,7 +56,7 @@ internal static bool TryConvertConnectionStringToSettings(string connectionStrin
if (connectionStrings.Length <= 3 && connectionStrings.Length > 0)
{
Regex SshRegex = new Regex(SshPrefixRegex);
- Regex dockerHostRegex = new Regex(DockerHostPrefixRegex);
+ Regex hostRegex = new Regex(HostPrefixRegex);
foreach (var item in connectionStrings)
{
@@ -66,9 +66,9 @@ internal static bool TryConvertConnectionStringToSettings(string connectionStrin
Match match = SshRegex.Match(segment);
remoteConnection = ConnectionManager.GetSSHConnection(segment.Substring(match.Length));
}
- else if (dockerHostRegex.IsMatch(segment))
+ else if (hostRegex.IsMatch(segment))
{
- Match match = dockerHostRegex.Match(segment);
+ Match match = hostRegex.Match(segment);
hostName = segment.Substring(match.Length);
}
else if (segment.Contains("="))
@@ -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
deleted file mode 100644
index 810975773..000000000
--- a/src/SSHDebugPS/Docker/DockerContainerInstance.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-// 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.Diagnostics;
-using Microsoft.DebugEngineHost;
-using Microsoft.SSHDebugPS.Utilities;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace Microsoft.SSHDebugPS.Docker
-{
- public class DockerContainerInstance : ContainerInstance
- {
- ///
- /// Create a DockerContainerInstance from the results of docker ps in JSON format
- ///
- public static bool TryCreate(string json, out DockerContainerInstance instance)
- {
- instance = null;
- try
- {
- JObject obj = JObject.Parse(json);
- instance = obj.ToObject();
- }
- catch (Exception e)
- {
- HostTelemetry.SendEvent(TelemetryHelper.Event_DockerPSParseFailure, new KeyValuePair[] {
- new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name)
- });
-
- string error = e.ToString();
- VsOutputWindowWrapper.WriteLine(StringResources.Error_DockerPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Docker_PSName);
- Debug.Fail(error);
- }
- return instance != null;
- }
-
- private DockerContainerInstance() { }
-
- #region JsonProperties
-
- [JsonProperty("ID")]
- public override string Id { get; set; }
-
- [JsonProperty("Names")]
- public override string Name { get; set; }
-
- [JsonProperty(nameof(Image))]
- public string Image { get; private set; }
-
- [JsonProperty(nameof(Ports))]
- public string Ports { get; set; }
-
- [JsonProperty(nameof(Command))]
- public string Command { get; private set; }
-
- [JsonProperty(nameof(Status))]
- public string Status { get; private set; }
-
- [JsonProperty("CreatedAt")]
- public string Created { get; private set; }
-
- [JsonIgnore]
- public string Platform { get; set; }
-
- #endregion
-
- // Docker container names: only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed. It is also case sensitive
- protected override bool EqualsInternal(ContainerInstance instance)
- {
- if (instance is DockerContainerInstance other)
- {
- // the id can be a partial on a container
- return String.Equals(Id, other.Id, StringComparison.Ordinal) ||
- Id.StartsWith(other.Id, StringComparison.Ordinal) ||
- other.Id.StartsWith(Id, StringComparison.Ordinal);
- }
-
- return false;
- }
-
- protected override int GetHashCodeInternal()
- {
- // Since IDs can be partial, we don't have a good way to get a good hash code.
- return string.IsNullOrWhiteSpace(Id) ? 0 : Id.Substring(0,1).GetHashCode();
- }
- }
-}
diff --git a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs
new file mode 100644
index 000000000..e6847967d
--- /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 (ContainerInstance 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 (ContainerInstance container in containers)
+ {
+ container.Platform = platform;
+ }
+ }
+ }
+ else
+ {
+ foreach (ContainerInstance 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..c433cb9d6 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;
});
@@ -174,20 +174,20 @@ internal static bool TryGetContainerPlatform(string hostname, string containerNa
return true;
}
- internal static IEnumerable GetLocalDockerContainers(string hostname, out int totalContainers)
+ internal static IEnumerable GetLocalDockerContainers(string hostname, out int totalContainers)
{
totalContainers = 0;
int containerCount = 0;
- List containers = new List();
+ List containers = new List();
DockerCommandSettings settings = new DockerCommandSettings(hostname, false);
settings.SetCommand(dockerPSCommand, dockerPSArgs);
- RunDockerCommand(settings, delegate (string args)
+ RunContainerCommand(settings, delegate (string args)
{
if (args.Trim()[0] == '{')
{
- if (DockerContainerInstance.TryCreate(args, out DockerContainerInstance containerInstance))
+ if (ContainerInstance.TryCreate(args, out ContainerInstance containerInstance))
{
containers.Add(containerInstance);
}
@@ -205,7 +205,7 @@ internal static IEnumerable GetLocalDockerContainers(st
// Another fallback option would be to: docker inspect --format {{.State.Status}} which should return "running"
internal static bool IsContainerRunning(string hostName, string containerName, Connection remoteConnection)
{
- IEnumerable containers;
+ IEnumerable containers;
if (remoteConnection != null)
{
containers = GetRemoteDockerContainers(remoteConnection, hostName, out _);
@@ -228,7 +228,7 @@ internal static bool IsContainerRunning(string hostName, string containerName, C
return false;
}
- internal static IEnumerable GetRemoteDockerContainers(IConnection connection, string hostname, out int totalContainers)
+ internal static IEnumerable GetRemoteDockerContainers(IConnection connection, string hostname, out int totalContainers)
{
totalContainers = 0;
SSHConnection sshConnection = connection as SSHConnection;
@@ -239,7 +239,7 @@ internal static IEnumerable GetRemoteDockerContainers(I
return null;
}
- List containers = new List();
+ List containers = new List();
DockerCommandSettings settings = new DockerCommandSettings(hostname, true);
settings.SetCommand(dockerPSCommand, dockerPSArgs);
@@ -300,7 +300,7 @@ internal static IEnumerable GetRemoteDockerContainers(I
foreach (var item in outputLines)
{
- if (DockerContainerInstance.TryCreate(item, out DockerContainerInstance containerInstance))
+ if (ContainerInstance.TryCreate(item, out ContainerInstance containerInstance))
{
containers.Add(containerInstance);
}
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..c64cff6ec
--- /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/Microsoft.SSHDebugPS.pkgdef b/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef
index 66c5f6034..bd613b575 100644
--- a/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef
+++ b/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef
@@ -7,6 +7,11 @@
"PortPickerCLSID"="{91BDF293-E6A0-49C4-B033-6F36CFC4FF98}"
"Name"="Docker (Linux Container)"
+[$RootKey$\AD7Metrics\PortSupplier\{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}]
+"CLSID"="{C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E}"
+"PortPickerCLSID"="{E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C}"
+"Name"="Podman (Linux Container)"
+
[$RootKey$\AD7Metrics\PortSupplier\{267B1341-AC92-44DC-94DF-2EE4205DD17E}]
"CLSID"="{B8587A49-00BD-4DEE-94B9-6EBF49003E04}"
"Name"="Windows Subsystem for Linux (WSL)"
@@ -47,6 +52,18 @@
"InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL"
"CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll"
+[$RootKey$\CLSID\{C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E}]
+"Assembly"="Microsoft.SSHDebugPS"
+"Class"="Microsoft.SSHDebugPS.Podman.PodmanPortSupplier"
+"InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL"
+"CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll"
+
+[$RootKey$\CLSID\{E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C}]
+"Assembly"="Microsoft.SSHDebugPS"
+"Class"="Microsoft.SSHDebugPS.Podman.PodmanLinuxPortPicker"
+"InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL"
+"CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll"
+
[$RootKey$\RuntimeConfiguration\dependentAssembly\codeBase\{7E3052B2-FB42-4E38-B22C-1FD281BD4413}]
"name"="Microsoft.SSHDebugPS"
; With local development workflow and release workflow, there are two publicKeyTokens but no way to specify both.
diff --git a/src/SSHDebugPS/Podman/PodmanConnection.cs b/src/SSHDebugPS/Podman/PodmanConnection.cs
new file mode 100644
index 000000000..839a776e3
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanConnection.cs
@@ -0,0 +1,155 @@
+// 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.IO;
+using System.Threading;
+using System.Diagnostics;
+using Microsoft.SSHDebugPS.Docker;
+using Microsoft.SSHDebugPS.Utilities;
+using Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier;
+using Microsoft.VisualStudio.Shell;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ internal sealed class PodmanConnection : PipeConnection
+ {
+ #region Statics
+
+ internal static string CreateConnectionString(string containerName, string remoteConnectionName, string hostName)
+ {
+ // Reuses the same format as Docker connection strings
+ return DockerConnection.CreateConnectionString(containerName, remoteConnectionName, hostName);
+ }
+
+ internal static bool TryConvertConnectionStringToSettings(string connectionString, out PodmanContainerTransportSettings settings, out Connection remoteConnection)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+
+ if (DockerConnection.TryConvertConnectionStringToSettings(connectionString, out DockerContainerTransportSettings dockerSettings, out remoteConnection))
+ {
+ settings = new PodmanContainerTransportSettings(dockerSettings.HostName, dockerSettings.ContainerName, remoteConnection != null);
+ return true;
+ }
+
+ settings = null;
+ return false;
+ }
+
+ #endregion
+
+ private readonly string _containerName;
+ private readonly PodmanExecutionManager _executionManager;
+ private readonly PodmanContainerTransportSettings _settings;
+
+ public PodmanConnection(PodmanContainerTransportSettings settings, Connection outerConnection, string name)
+ : base(outerConnection, name)
+ {
+ _settings = settings;
+ _containerName = settings.ContainerName;
+ _executionManager = new PodmanExecutionManager(settings, outerConnection);
+ }
+
+ public override int ExecuteCommand(string commandText, int timeout, out string commandOutput, out string errorMessage)
+ {
+ return _executionManager.ExecuteCommand(commandText, timeout, out commandOutput, out errorMessage);
+ }
+
+ ///
+ public override void BeginExecuteAsyncCommand(string commandText, bool runInShell, IDebugUnixShellCommandCallback callback, out IDebugUnixShellAsyncCommand asyncCommand)
+ {
+ if (IsClosed)
+ {
+ throw new ObjectDisposedException(nameof(PipeConnection));
+ }
+
+ var commandRunner = GetExecCommandRunner(commandText, handleRawOutput: runInShell == false);
+ asyncCommand = new PipeAsyncCommand(commandRunner, callback);
+ }
+
+ public override void CopyFile(string sourcePath, string destinationPath)
+ {
+ PodmanCopySettings settings;
+ string tmpFile = null;
+
+ if (!Directory.Exists(sourcePath) && !File.Exists(sourcePath))
+ {
+ throw new ArgumentException(StringResources.Error_CopyFile_SourceNotFound.FormatCurrentCultureWithArgs(sourcePath), nameof(sourcePath));
+ }
+
+ if (OuterConnection != null)
+ {
+ tmpFile = "/tmp" + "/" + StringResources.CopyFile_TempFilePrefix + Guid.NewGuid();
+ OuterConnection.CopyFile(sourcePath, tmpFile);
+ settings = new PodmanCopySettings(_settings, tmpFile, destinationPath);
+ }
+ else
+ {
+ settings = new PodmanCopySettings(_settings, sourcePath, destinationPath);
+ }
+
+ ICommandRunner runner = GetCommandRunner(settings);
+
+ ManualResetEvent resetEvent = new ManualResetEvent(false);
+ int exitCode = -1;
+ runner.Closed += (e, args) =>
+ {
+ exitCode = args;
+ resetEvent.Set();
+ try
+ {
+ if (OuterConnection != null && !string.IsNullOrEmpty(tmpFile))
+ {
+ string output;
+ string errorMessage;
+ int exit = OuterConnection.ExecuteCommand("rm " + tmpFile, 5000, out output, out errorMessage);
+ Debug.Assert(exit == 0, FormattableString.Invariant($"Removing file exited with {exit} and message {output}. {errorMessage}"));
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Fail("Exception thrown while cleaning up temp file. " + ex.Message);
+ }
+ };
+
+ runner.Start();
+
+ bool complete = resetEvent.WaitOne(Timeout.Infinite);
+ if (!complete || exitCode != 0)
+ {
+ throw new CommandFailedException(StringResources.Error_CopyFileFailed);
+ }
+ }
+
+ public override string GetUserHomeDirectory()
+ {
+ return ExecuteCommand("eval echo '~'", Timeout.Infinite);
+ }
+
+ private ICommandRunner GetExecCommandRunner(string commandText, bool handleRawOutput = false)
+ {
+ var execSettings = new PodmanExecSettings(this._settings, commandText, handleRawOutput);
+ return GetCommandRunner(execSettings, handleRawOutput: handleRawOutput);
+ }
+
+ private ICommandRunner GetCommandRunner(IPipeTransportSettings settings, bool handleRawOutput = false)
+ {
+ if (OuterConnection == null)
+ {
+ return LocalCommandRunner.CreateInstance(handleRawOutput, settings);
+ }
+ else
+ {
+ return new RemoteCommandRunner(settings, OuterConnection, handleRawOutput);
+ }
+ }
+
+ protected override string ProcFSErrorMessage
+ {
+ get
+ {
+ return String.Concat(base.ProcFSErrorMessage, Environment.NewLine, StringResources.Error_EnsurePodmanContainerIsLinux);
+ }
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanContainerInstance.cs b/src/SSHDebugPS/Podman/PodmanContainerInstance.cs
new file mode 100644
index 000000000..72f004ee9
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanContainerInstance.cs
@@ -0,0 +1,63 @@
+// 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.Diagnostics;
+using System.Linq;
+using Microsoft.DebugEngineHost;
+using Microsoft.SSHDebugPS.Docker;
+using Microsoft.SSHDebugPS.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ public class PodmanContainerInstance : ContainerInstance
+ {
+ public static bool TryCreate(string json, out PodmanContainerInstance instance)
+ {
+ instance = null;
+ try
+ {
+ JObject obj = JObject.Parse(json);
+ instance = obj.ToObject();
+ }
+ catch (Exception e)
+ {
+ HostTelemetry.SendEvent(TelemetryHelper.Event_PodmanPSParseFailure, new KeyValuePair[] {
+ new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name)
+ });
+
+ string error = e.ToString();
+ VsOutputWindowWrapper.WriteLine(StringResources.Error_PodmanPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Podman_PSName);
+ Debug.Fail(error);
+ }
+ return instance != null;
+ }
+
+ [JsonProperty("Command")]
+ [JsonConverter(typeof(PodmanJsonConverter))]
+ public override string Command { get; protected set; }
+
+ [JsonProperty("Ports")]
+ [JsonConverter(typeof(PodmanJsonConverter))]
+ public override string Ports { get; set; }
+
+ [JsonProperty("Names")]
+ [JsonConverter(typeof(PodmanJsonConverter))]
+ public override string Name { get; set; }
+
+ protected override bool EqualsInternal(ContainerInstance instance)
+ {
+ if (instance is PodmanContainerInstance other)
+ {
+ return String.Equals(Id, other.Id, StringComparison.Ordinal) ||
+ Id.StartsWith(other.Id, StringComparison.Ordinal) ||
+ other.Id.StartsWith(Id, StringComparison.Ordinal);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs b/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs
new file mode 100644
index 000000000..6d4571e52
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs
@@ -0,0 +1,37 @@
+// 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;
+using Microsoft.SSHDebugPS.UI;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ internal sealed class PodmanDiscoveryStrategy : IContainerDiscoveryStrategy
+ {
+ public string ConnectionLabel => UIResources.Podman_ConnectionLabel;
+ public string HostnameLabel => UIResources.Podman_HostnameLabel;
+ public string HostnameTip => UIResources.Podman_HostnameTip;
+ public string ConnectionToolTip => UIResources.Podman_ConnectionToolTip;
+ public string HostnameAutomationName => UIResources.Podman_HostnameAutomationName;
+
+ public IEnumerable GetLocalContainers(string hostname, out int totalContainers)
+ {
+ return PodmanHelper.GetLocalPodmanContainers(hostname, out totalContainers);
+ }
+
+ public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers)
+ {
+ return PodmanHelper.GetRemotePodmanContainers(connection, hostname, out totalContainers);
+ }
+
+ public void AssignPlatforms(IEnumerable containers, string hostname)
+ {
+ // Podman only supports Linux containers
+ foreach (ContainerInstance container in containers)
+ {
+ container.Platform = "Linux";
+ }
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanExecutionManager.cs b/src/SSHDebugPS/Podman/PodmanExecutionManager.cs
new file mode 100644
index 000000000..aef6ccc8f
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanExecutionManager.cs
@@ -0,0 +1,19 @@
+// 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.Docker;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ internal sealed class PodmanExecutionManager : DockerExecutionManager
+ {
+ public PodmanExecutionManager(PodmanContainerTransportSettings baseSettings, Connection outerConnection)
+ : base(baseSettings, outerConnection)
+ { }
+
+ protected override ContainerExecSettings CreateExecSettings(ContainerTargetTransportSettings baseSettings, string command, bool runInShell, bool makeInteractive)
+ {
+ return new PodmanExecSettings((PodmanContainerTransportSettings)baseSettings, command, runInShell, makeInteractive);
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanHelper.cs b/src/SSHDebugPS/Podman/PodmanHelper.cs
new file mode 100644
index 000000000..5d25de158
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanHelper.cs
@@ -0,0 +1,155 @@
+// 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.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Microsoft.SSHDebugPS.Docker;
+using Microsoft.SSHDebugPS.SSH;
+using Microsoft.SSHDebugPS.Utilities;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ public class PodmanHelper
+ {
+ private const string podmanPSCommand = "ps";
+ private const string podmanPSArgs = "-f status=running --no-trunc --format \"{{json .}}\"";
+
+
+ internal static IEnumerable GetLocalPodmanContainers(string hostname, out int totalContainers)
+ {
+ totalContainers = 0;
+ int containerCount = 0;
+ List containers = new List();
+
+ PodmanCommandSettings settings = new PodmanCommandSettings(hostname, false);
+ settings.SetCommand(podmanPSCommand, podmanPSArgs);
+
+ DockerHelper.RunContainerCommand(settings, delegate (string args)
+ {
+ if (args.Trim()[0] == '{')
+ {
+ if (PodmanContainerInstance.TryCreate(args, out PodmanContainerInstance containerInstance))
+ {
+ containers.Add(containerInstance);
+ }
+ containerCount++;
+ }
+ });
+
+ totalContainers = containerCount;
+ return containers;
+ }
+
+ ///
+ /// Checks if the specified container is in the list of containers from the target host.
+ ///
+ internal static bool IsContainerRunning(string hostName, string containerName, Connection remoteConnection)
+ {
+ IEnumerable containers;
+ if (remoteConnection != null)
+ {
+ containers = GetRemotePodmanContainers(remoteConnection, hostName, out _);
+ }
+ else
+ {
+ containers = GetLocalPodmanContainers(hostName, out _);
+ }
+
+ if (containers != null)
+ {
+ if (containers.Any(container => string.Equals(container.Name, containerName, StringComparison.Ordinal)
+ || container.Id.StartsWith(containerName, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static IEnumerable GetRemotePodmanContainers(IConnection connection, string hostname, out int totalContainers)
+ {
+ totalContainers = 0;
+ SSHConnection sshConnection = connection as SSHConnection;
+ List outputLines = new List();
+ StringBuilder errorSB = new StringBuilder();
+ if (sshConnection == null)
+ {
+ return null;
+ }
+
+ List containers = new List();
+
+ PodmanCommandSettings settings = new PodmanCommandSettings(hostname, true);
+ settings.SetCommand(podmanPSCommand, podmanPSArgs);
+
+ RemoteCommandRunner commandRunner = new RemoteCommandRunner(settings, sshConnection, handleRawOutput: false);
+
+ ManualResetEvent resetEvent = new ManualResetEvent(false);
+ int exitCode = 0;
+ commandRunner.ErrorOccured += ((sender, args) =>
+ {
+ errorSB.Append(args);
+ });
+
+ commandRunner.Closed += ((sender, args) =>
+ {
+ exitCode = args;
+ resetEvent.Set();
+ });
+
+ commandRunner.OutputReceived += ((sender, line) =>
+ {
+ if (!string.IsNullOrWhiteSpace(line))
+ {
+ Debug.Assert(line.IndexOf('\n') < 0, "Why does `line` have embedded newline characters?");
+
+ if (line.Trim()[0] != '{')
+ {
+ errorSB.Append(line);
+ }
+
+ outputLines.Add(line);
+ }
+ });
+
+ commandRunner.Start();
+
+ bool cancellationRequested = false;
+ VS.VSOperationWaiter.Wait(UIResources.QueryingForContainersMessage, false, (cancellationToken) =>
+ {
+ while (!resetEvent.WaitOne(2000) && !cancellationToken.IsCancellationRequested)
+ { }
+ cancellationRequested = cancellationToken.IsCancellationRequested;
+ });
+
+ if (!cancellationRequested)
+ {
+ if (exitCode != 0)
+ {
+ string exceptionMessage = UIResources.CommandExecutionErrorWithExitCodeFormat.FormatCurrentCultureWithArgs(
+ "{0} {1}".FormatInvariantWithArgs(settings.Command, settings.CommandArgs),
+ exitCode,
+ errorSB.ToString());
+
+ throw new CommandFailedException(exceptionMessage);
+ }
+
+ foreach (var item in outputLines)
+ {
+ if (PodmanContainerInstance.TryCreate(item, out PodmanContainerInstance containerInstance))
+ {
+ containers.Add(containerInstance);
+ totalContainers++;
+ }
+ }
+ }
+
+ return containers;
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanJsonConverter.cs b/src/SSHDebugPS/Podman/PodmanJsonConverter.cs
new file mode 100644
index 000000000..c82819bba
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanJsonConverter.cs
@@ -0,0 +1,52 @@
+// 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.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ // Handles JSON values that may be a string, array of strings, or array of objects (port mappings).
+ internal sealed class PodmanJsonConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType) => objectType == typeof(string);
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var token = JToken.Load(reader);
+ switch (token.Type)
+ {
+ case JTokenType.String:
+ return token.Value();
+ case JTokenType.Array:
+ return string.Join(", ", token.Select(t =>
+ {
+ if (t.Type == JTokenType.String)
+ return t.Value();
+ if (t.Type == JTokenType.Object)
+ {
+ // Handle Podman port mapping objects: {"host_ip":"0.0.0.0","container_port":80,"host_port":8080,"range":1,"protocol":"tcp"}
+ var hostIp = t.Value("host_ip") ?? "0.0.0.0";
+ var hostPort = t.Value("host_port");
+ var containerPort = t.Value("container_port");
+ var protocol = t.Value("protocol") ?? "tcp";
+ if (hostPort.HasValue && containerPort.HasValue)
+ return $"{hostIp}:{hostPort}->{containerPort}/{protocol}";
+ }
+ return t.ToString();
+ }));
+ case JTokenType.Null:
+ return string.Empty;
+ default:
+ return token.ToString();
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ writer.WriteValue(value?.ToString());
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanPort.cs b/src/SSHDebugPS/Podman/PodmanPort.cs
new file mode 100644
index 000000000..edda987a3
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanPort.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.VisualStudio.Shell;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ internal sealed class PodmanPort : AD7Port
+ {
+ public PodmanPort(AD7PortSupplier portSupplier, string name, bool isInAddPort)
+ : base(portSupplier, name, isInAddPort)
+ { }
+
+ protected override Connection GetConnectionInternal()
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ return ConnectionManager.GetPodmanConnection(Name, supportSSHConnections: true);
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanPortPicker.cs b/src/SSHDebugPS/Podman/PodmanPortPicker.cs
new file mode 100644
index 000000000..cd4fd2868
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanPortPicker.cs
@@ -0,0 +1,18 @@
+// 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.Runtime.InteropServices;
+using Microsoft.SSHDebugPS.Docker;
+using Microsoft.VisualStudio.Shell;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ [ComVisible(true)]
+ [Guid("E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C")]
+ public class PodmanLinuxPortPicker : DockerPortPickerBase
+ {
+ internal override bool SupportSSHConnections => true;
+ internal override ContainerRuntimeType RuntimeType => ContainerRuntimeType.Podman;
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanPortSupplier.cs b/src/SSHDebugPS/Podman/PodmanPortSupplier.cs
new file mode 100644
index 000000000..82031b256
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanPortSupplier.cs
@@ -0,0 +1,58 @@
+// 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.Runtime.InteropServices;
+using Microsoft.VisualStudio.Debugger.Interop;
+
+namespace Microsoft.SSHDebugPS.Podman
+{
+ [ComVisible(true)]
+ [Guid("C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E")]
+ internal sealed class PodmanPortSupplier : AD7PortSupplier
+ {
+ private readonly Guid _Id = new Guid("D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A");
+
+ protected override Guid Id { get { return _Id; } }
+ protected override string Name { get { return StringResources.Podman_PSName; } }
+ protected override string Description { get { return StringResources.Podman_PSDescription; } }
+
+ public PodmanPortSupplier() : base()
+ { }
+
+ public override int AddPort(IDebugPortRequest2 request, out IDebugPort2 port)
+ {
+ string name;
+ HR.Check(request.GetPortName(out name));
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ AD7Port newPort = new PodmanPort(this, name, isInAddPort: true);
+
+ if (newPort.IsConnected)
+ {
+ port = newPort;
+ return HR.S_OK;
+ }
+ }
+
+ port = null;
+ return HR.E_REMOTE_CONNECT_USER_CANCELED;
+ }
+
+ public override unsafe int EnumPersistedPorts(BSTR_ARRAY portNames, out IEnumDebugPorts2 portEnum)
+ {
+ IDebugPort2[] ports = new IDebugPort2[portNames.dwCount];
+ for (int c = 0; c < portNames.dwCount; c++)
+ {
+ char* bstrPortName = ((char**)portNames.Members)[c];
+ string name = new string(bstrPortName);
+
+ ports[c] = new PodmanPort(this, name, isInAddPort: false);
+ }
+
+ portEnum = new AD7PortEnum(ports);
+ return HR.S_OK;
+ }
+ }
+}
diff --git a/src/SSHDebugPS/Podman/PodmanTransportSettings.cs b/src/SSHDebugPS/Podman/PodmanTransportSettings.cs
new file mode 100644
index 000000000..1dd1d1248
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanTransportSettings.cs
@@ -0,0 +1,46 @@
+// 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.Podman
+{
+ internal sealed class PodmanContainerTransportSettings : ContainerTargetTransportSettings
+ {
+ internal const string WindowsExeName = "podman.exe";
+ internal const string UnixExeName = "podman";
+ internal const string HostFlag = "--url \"{0}\"";
+
+ public PodmanContainerTransportSettings(string hostname, string containerName, bool hostIsUnix)
+ : base(hostname, containerName, hostIsUnix, WindowsExeName, UnixExeName, HostFlag)
+ { }
+
+ public PodmanContainerTransportSettings(PodmanContainerTransportSettings settings)
+ : base(settings)
+ { }
+ }
+
+ internal sealed class PodmanExecSettings : ContainerExecSettings
+ {
+ public PodmanExecSettings(PodmanContainerTransportSettings settings, string command, bool runInShell, bool makeInteractive = true)
+ : base(settings, command, runInShell, makeInteractive)
+ { }
+ }
+
+ internal sealed class PodmanCopySettings : ContainerCopySettings
+ {
+ public PodmanCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix)
+ : base(hostname, sourcePath, destinationPath, containerName, hostIsUnix, PodmanContainerTransportSettings.WindowsExeName, PodmanContainerTransportSettings.UnixExeName, PodmanContainerTransportSettings.HostFlag)
+ { }
+
+ public PodmanCopySettings(PodmanContainerTransportSettings settings, string sourcePath, string destinationPath)
+ : base(settings, sourcePath, destinationPath)
+ { }
+ }
+
+ internal sealed class PodmanCommandSettings : ContainerCommandSettings
+ {
+ public PodmanCommandSettings(string hostname, bool hostIsUnix)
+ : base(hostname, hostIsUnix, PodmanContainerTransportSettings.WindowsExeName, PodmanContainerTransportSettings.UnixExeName, PodmanContainerTransportSettings.HostFlag)
+ { }
+ }
+}
+
diff --git a/src/SSHDebugPS/StringResources.Designer.cs b/src/SSHDebugPS/StringResources.Designer.cs
index 78a19bb65..5706b852b 100644
--- a/src/SSHDebugPS/StringResources.Designer.cs
+++ b/src/SSHDebugPS/StringResources.Designer.cs
@@ -114,6 +114,33 @@ internal static string Docker_PSName {
}
}
+ ///
+ /// Looks up a localized string similar to Podman (Linux Container).
+ ///
+ internal static string Podman_PSName {
+ get {
+ return ResourceManager.GetString("Podman_PSName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Podman (Linux Container) connection type allows Visual Studio to connect to Podman containers running locally or remotely (using SSH)..
+ ///
+ internal static string Podman_PSDescription {
+ get {
+ return ResourceManager.GetString("Podman_PSDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to parse output of '{0}': {1}.
+ ///
+ internal static string Error_PodmanPSParseFailed {
+ get {
+ return ResourceManager.GetString("Error_PodmanPSParseFailed", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Command failed to execute.
///
@@ -195,6 +222,15 @@ internal static string Error_EnsureDockerContainerIsLinux {
}
}
+ ///
+ /// Looks up a localized string similar to Ensure the selected Podman Connection target is a Linux container..
+ ///
+ internal static string Error_EnsurePodmanContainerIsLinux {
+ get {
+ return ResourceManager.GetString("Error_EnsurePodmanContainerIsLinux", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Unable to parse exit code..
///
diff --git a/src/SSHDebugPS/StringResources.resx b/src/SSHDebugPS/StringResources.resx
index af44a77c5..571c143fb 100644
--- a/src/SSHDebugPS/StringResources.resx
+++ b/src/SSHDebugPS/StringResources.resx
@@ -139,6 +139,16 @@
Docker (Linux Container)
+
+ Podman (Linux Container)
+
+
+ The Podman (Linux Container) connection type allows Visual Studio to connect to Podman containers running locally or remotely (using SSH).
+
+
+ Failed to parse output of '{0}': {1}
+ {0} is the JSON line that failed to parse. {1} is the exception details.
+
Command failed to execute
@@ -165,6 +175,9 @@
Ensure the selected Docker Connection target is a Linux container.
+
+ Ensure the selected Podman Connection target is a Linux container.
+
Failed to parse json '{0}'.\r\nError: '{1}'
{0} is a json output item from the output 'docker ps' and {1} is the error message
diff --git a/src/SSHDebugPS/UI/ContainerInstance.cs b/src/SSHDebugPS/UI/ContainerInstance.cs
index 4ec33929e..fa182104a 100644
--- a/src/SSHDebugPS/UI/ContainerInstance.cs
+++ b/src/SSHDebugPS/UI/ContainerInstance.cs
@@ -1,79 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Globalization;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
-
namespace Microsoft.SSHDebugPS.Docker
{
- public interface IContainerInstance : IEquatable
+ public interface IContainerInstance : System.IEquatable
{
string Id { get; }
string Name { get; }
}
-
- public abstract class ContainerInstance : IContainerInstance
- {
- public abstract string Id { get; set; }
- public abstract string Name { get; set; }
-
- #region IEquatable
-
- public static bool operator ==(ContainerInstance left, ContainerInstance right)
- {
- if (left is null || right is null)
- {
- return ReferenceEquals(left, right);
- }
-
- return left.Equals(right);
- }
-
- public static bool operator !=(ContainerInstance left, ContainerInstance right)
- {
- return !(left == right);
- }
-
- public bool Equals(IContainerInstance instance)
- {
- if (!ReferenceEquals(null, instance) && instance is ContainerInstance container)
- {
- return this.EqualsInternal(container);
- }
-
- return false;
- }
-
- public override bool Equals(object obj)
- {
- if (obj is IContainerInstance instance)
- {
- return this.Equals(instance);
- }
- return false;
- }
-
- public override int GetHashCode()
- {
- return GetHashCodeInternal();
- }
-
- #endregion
-
- #region Helper Methods
-
- protected abstract bool EqualsInternal(ContainerInstance instance);
- protected abstract int GetHashCodeInternal();
-
- #endregion
- }
}
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/UIResources.Designer.cs b/src/SSHDebugPS/UI/UIResources.Designer.cs
index 12e90b5c2..fce792a7a 100644
--- a/src/SSHDebugPS/UI/UIResources.Designer.cs
+++ b/src/SSHDebugPS/UI/UIResources.Designer.cs
@@ -267,6 +267,51 @@ public static string HostnameAutomationName {
}
}
+ ///
+ /// Looks up a localized string similar to Podman _CLI host:.
+ ///
+ public static string Podman_ConnectionLabel {
+ get {
+ return ResourceManager.GetString("Podman_ConnectionLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Podman _host (Optional):.
+ ///
+ public static string Podman_HostnameLabel {
+ get {
+ return ResourceManager.GetString("Podman_HostnameLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Specify a URL for connecting to a different Podman host. .
+ ///
+ public static string Podman_HostnameTip {
+ get {
+ return ResourceManager.GetString("Podman_HostnameTip", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Location from which to run the Podman CLI....
+ ///
+ public static string Podman_ConnectionToolTip {
+ get {
+ return ResourceManager.GetString("Podman_ConnectionToolTip", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Optional Podman Host name.
+ ///
+ public static string Podman_HostnameAutomationName {
+ get {
+ return ResourceManager.GetString("Podman_HostnameAutomationName", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Docker _host (Optional):.
///
diff --git a/src/SSHDebugPS/UI/UIResources.resx b/src/SSHDebugPS/UI/UIResources.resx
index 7ce103459..e06689608 100644
--- a/src/SSHDebugPS/UI/UIResources.resx
+++ b/src/SSHDebugPS/UI/UIResources.resx
@@ -239,6 +239,22 @@
Optional Docker Host name
+
+ Podman _CLI host:
+
+
+ Podman _host (Optional):
+ Hostname for Podman daemon configuration
+
+
+ Specify a URL for connecting to a different Podman host.
+
+
+ Location from which to run the Podman CLI. To manage remote connections, in the menu go to Tools -> Options and find Cross Platform -> Connection Manager.
+
+
+ Optional Podman Host name
+
Container List
diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs
index 2fefd0890..ea58f08de 100644
--- a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs
+++ b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs
@@ -11,6 +11,7 @@
using System.Windows.Threading;
using liblinux.Persistence;
using Microsoft.SSHDebugPS.Docker;
+using Microsoft.SSHDebugPS.Podman;
using Microsoft.SSHDebugPS.SSH;
using Microsoft.SSHDebugPS.Utilities;
using System.Globalization;
@@ -22,10 +23,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 +144,31 @@ 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();
+ case ContainerRuntimeType.Podman:
+ return new PodmanDiscoveryStrategy();
+ 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 +178,19 @@ private void RefreshContainersListInternal()
IContainerViewModel selectedContainer = SelectedContainerInstance;
SelectedContainerInstance = null;
- IEnumerable containers;
+ 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 +201,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)
diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs
index b36798a10..1a0f4df51 100644
--- a/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs
+++ b/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs
@@ -131,9 +131,9 @@ public bool IsSelected
}
public class DockerContainerViewModel
- : ContainerViewModel
+ : ContainerViewModel
{
- public DockerContainerViewModel(DockerContainerInstance instance)
+ public DockerContainerViewModel(ContainerInstance instance)
: base(instance)
{ }
diff --git a/src/SSHDebugPS/Utilities/TelemetryHelper.cs b/src/SSHDebugPS/Utilities/TelemetryHelper.cs
index 85a13f327..8f019b1c2 100644
--- a/src/SSHDebugPS/Utilities/TelemetryHelper.cs
+++ b/src/SSHDebugPS/Utilities/TelemetryHelper.cs
@@ -6,6 +6,7 @@ namespace Microsoft.SSHDebugPS.Utilities
internal static class TelemetryHelper
{
public const string Event_DockerPSParseFailure = @"VS/Diagnostics/Debugger/SSHDebugPS/DockerPSParseFailure";
+ public const string Event_PodmanPSParseFailure = @"VS/Diagnostics/Debugger/SSHDebugPS/PodmanPSParseFailure";
public const string Event_ProcFSError = @"VS/Diagnostics/Debugger/SSHDebugPS/ProcFSError";
public static readonly string Property_ExceptionName = "vs.diagnostics.debugger.ExceptionName";
}