From 6598c41312879774ffa94fa226e03d5815301bdb Mon Sep 17 00:00:00 2001 From: brdeyo Date: Tue, 8 Sep 2026 17:50:45 -0700 Subject: [PATCH] Add SDK_EXPERIMENT_ID and SDK_METADATA environment variables to processes on creation. --- VERSION | 2 +- .../ElasticsearchRallyServerExecutorTests.cs | 20 --- .../ElasticsearchRallyServerExecutor.cs | 5 +- .../MongoDB/MongoDBServerExecutor.cs | 53 +++--- .../VirtualClient.Contracts/Constants.cs | 10 ++ .../VirtualClientComponentExtensionsTests.cs | 165 +++++++++++++++++- .../WindowsDiskManagerTests.cs | 20 +-- .../Components/ExecuteCommand.cs | 2 + .../Components/ExecuteCommandMonitor.cs | 1 + .../Components/ExecuteCommandMonitor_Shell.cs | 1 + .../Components/ExecuteCommand_Shell.cs | 1 + .../VirtualClient.Core/DiskManager.cs | 2 +- .../VirtualClient.Core/IDiskManager.cs | 4 +- .../VirtualClient.Core/UnixDiskManager.cs | 3 +- .../VirtualClientComponentExtensions.cs | 21 +++ .../VirtualClient.Core/WindowsDiskManager.cs | 10 +- .../SetDiskSanPolicyTests.cs | 6 +- .../InMemoryDiskManager.cs | 3 +- 18 files changed, 249 insertions(+), 80 deletions(-) diff --git a/VERSION b/VERSION index fbcbf73806..8cf6caf561 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 \ No newline at end of file +3.4.1 \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/ElasticsearchRally/ElasticsearchRallyServerExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/ElasticsearchRally/ElasticsearchRallyServerExecutorTests.cs index 96e587be49..f938734a01 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/ElasticsearchRally/ElasticsearchRallyServerExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/ElasticsearchRally/ElasticsearchRallyServerExecutorTests.cs @@ -57,26 +57,6 @@ public void SetupTest() }; } - [Test] - public void TestElasticsearchRallyServerExecutorInitializeYmlNotFound() - { - SetupTest(); - - bool commandExecuted = false; - - using (TestElasticsearchRallyServerExecutor executor = new TestElasticsearchRallyServerExecutor(this.Dependencies, this.Parameters)) - { - executor.OnRunCommand = (command, arguments) => - { - commandExecuted = true; - }; - - Assert.ThrowsAsync(() => executor.InitializeAsync(EventContext.None, CancellationToken.None)); - } - - Assert.IsTrue(commandExecuted); - } - [Test] [TestCase(true)] [TestCase(false)] diff --git a/src/VirtualClient/VirtualClient.Actions/ElasticsearchRally/ElasticsearchRallyServerExecutor.cs b/src/VirtualClient/VirtualClient.Actions/ElasticsearchRally/ElasticsearchRallyServerExecutor.cs index f9cda86db5..6f427e9bae 100644 --- a/src/VirtualClient/VirtualClient.Actions/ElasticsearchRally/ElasticsearchRallyServerExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/ElasticsearchRally/ElasticsearchRallyServerExecutor.cs @@ -121,7 +121,7 @@ private async Task StartElasticsearch(EventContext telemetryContext, Cancellatio } } - private void StartElasticsearchLinux(EventContext telemetryContext, CancellationToken cancellationToken, string mountPoint) + private async Task StartElasticsearchLinux(EventContext telemetryContext, CancellationToken cancellationToken, string mountPoint) { string scriptsDirectory = this.PlatformSpecifics.GetScriptPath(this.PackageName.ToLower()); int port = this.Port; @@ -176,7 +176,8 @@ private void StartElasticsearchLinux(EventContext telemetryContext, Cancellation this.RunCommandAsRoot(telemetryContext, cancellationToken, "ElasticsearchDaemonReexec", "systemctl daemon-reexec"); this.RunCommandAsRoot(telemetryContext, cancellationToken, "ElasticsearchEnable", "systemctl enable elasticsearch"); bool ok = this.RunCommandAsRoot(telemetryContext, cancellationToken, "ElasticsearchStart", "systemctl start elasticsearch.service"); - Thread.Sleep(30000); // wait for elasticsearch to start + + await this.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken); // wait for elasticsearch to start if (!ok) { diff --git a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs index a4d6544b0d..f2c14d4516 100644 --- a/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/MongoDB/MongoDBServerExecutor.cs @@ -74,25 +74,24 @@ public string DiskFilter /// protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) { - await base.InitializeAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await base.InitializeAsync(telemetryContext, cancellationToken); this.InitializeApiClients(); - await MongoDBServerExecutor.OpenFirewallPortsAsync(this.Port, this.systemManagement.FirewallManager, cancellationToken) - .ConfigureAwait(false); + await MongoDBServerExecutor.OpenFirewallPortsAsync(this.Port, this.systemManagement.FirewallManager, cancellationToken); // Initialize disk if DiskFilter is specified if (!string.IsNullOrWhiteSpace(this.DiskFilter)) { - await this.InitializeDiskPathAsync(cancellationToken).ConfigureAwait(false); - await this.ConfigureDiskForMongoDBAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.InitializeDiskPathAsync(cancellationToken); + await this.ConfigureDiskForMongoDBAsync(telemetryContext, cancellationToken); } // Ensure MongoDB is configured to listen on all interfaces - await this.ConfigureMongoDBBindAddressAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.ConfigureMongoDBBindAddressAsync(telemetryContext, cancellationToken); // Start MongoDB server - await this.StartMongoDBServerAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.StartMongoDBServerAsync(telemetryContext, cancellationToken); } /// @@ -106,14 +105,13 @@ protected override Task ExecuteAsync(EventContext telemetryContext, Cancellation { this.SetServerOnline(false); - await this.ServerApiClient.PollForHeartbeatAsync(TimeSpan.FromMinutes(5), cancellationToken) - .ConfigureAwait(false); + await this.ServerApiClient.PollForHeartbeatAsync(TimeSpan.FromMinutes(5), cancellationToken); // Server is now online and ready to accept connections this.SetServerOnline(true); // Keep the server running until cancelled - await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false); + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); } catch (OperationCanceledException) { @@ -174,7 +172,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"{setBindIpCmd}\"", "ConfigureBindAddress", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); this.Logger.LogMessage( $"{nameof(MongoDBServerExecutor)}.BindAddressConfigured", @@ -201,13 +199,13 @@ private async Task StartMongoDBServerAsync(EventContext telemetryContext, Cancel try { // Restart MongoDB to apply all configurations - await this.ExecuteMongoDBServiceCommandAsync("restart", telemetryContext, cancellationToken).ConfigureAwait(false); + await this.ExecuteMongoDBServiceCommandAsync("restart", telemetryContext, cancellationToken); // Wait a bit for MongoDB to fully start - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); + await this.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); // Verify MongoDB is running - await this.VerifyMongoDBIsRunningAsync(telemetryContext, cancellationToken).ConfigureAwait(false); + await this.VerifyMongoDBIsRunningAsync(telemetryContext, cancellationToken); this.Logger.LogMessage( $"{nameof(MongoDBServerExecutor)}.MongoDBServerStarted", @@ -230,7 +228,7 @@ private async Task VerifyMongoDBIsRunningAsync(EventContext telemetryContext, Ca using (IProcessProxy process = this.systemManagement.ProcessManager.CreateElevatedProcess( this.Platform, "mongosh", "--eval \"db.runCommand({ping: 1})\"")) { - await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + await process.StartAndWaitAsync(cancellationToken); if (process.ExitCode != 0) { @@ -272,7 +270,7 @@ private async Task ConfigureDiskForMongoDBAsync(EventContext telemetryContext, C string mongoDataPath = "/mnt/mongodb-data"; // Stop MongoDB service before mounting - await this.ExecuteMongoDBServiceCommandAsync("stop", telemetryContext, cancellationToken).ConfigureAwait(false); + await this.ExecuteMongoDBServiceCommandAsync("stop", telemetryContext, cancellationToken); // Create filesystem on disk await this.ExecuteMongoDBCommandAsync( @@ -280,7 +278,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"sudo mkfs.ext4 -F {diskDevicePath}\"", "CreateFilesystem", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); // Create mount point await this.ExecuteMongoDBCommandAsync( @@ -288,7 +286,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"sudo mkdir -p {mongoDataPath}\"", "CreateMountPoint", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); // Mount the disk await this.ExecuteMongoDBCommandAsync( @@ -296,7 +294,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"sudo mount -t ext4 {diskDevicePath} {mongoDataPath}\"", "MountDisk", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); // Set permissions. The MongoDB service account name differs by package format: // Debian/Ubuntu packages create 'mongodb' whereas RPM-based distributions @@ -310,7 +308,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"{resolveServiceUser}\"", "SetPermissions", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); // Update mongod.conf to use the new dbPath string configFile = "/etc/mongod.conf"; @@ -320,7 +318,7 @@ await this.ExecuteMongoDBCommandAsync( $"-c \"{setDbPathCmd}\"", "UpdateMongoConf", telemetryContext, - cancellationToken).ConfigureAwait(false); + cancellationToken); this.Logger.LogMessage( $"{nameof(MongoDBServerExecutor)}.DiskConfigurationComplete", @@ -353,12 +351,11 @@ private async Task ExecuteMongoDBCommandAsync( this.CleanupTasks.Add(() => process.SafeKill()); this.LogProcessTrace(process); - await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + await process.StartAndWaitAsync(cancellationToken); if (!cancellationToken.IsCancellationRequested) { - await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-{scenario}", logToFile: true) - .ConfigureAwait(false); + await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-{scenario}", logToFile: true); if (process.ExitCode != 0) { @@ -395,12 +392,11 @@ private async Task ExecuteMongoDBServiceCommandAsync(string action, EventContext this.CleanupTasks.Add(() => process.SafeKill()); this.LogProcessTrace(process); - await process.StartAndWaitAsync(cancellationToken).ConfigureAwait(false); + await process.StartAndWaitAsync(cancellationToken); if (!cancellationToken.IsCancellationRequested) { - await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-Service-{action}", logToFile: true) - .ConfigureAwait(false); + await this.LogProcessDetailsAsync(process, telemetryContext, $"MongoDBServer-Service-{action}", logToFile: true); if (process.ExitCode != 0) { @@ -433,8 +429,7 @@ private async Task InitializeDiskPathAsync(CancellationToken cancellationToken) return; } - IEnumerable disks = await this.systemManagement.DiskManager.GetDisksAsync(cancellationToken) - .ConfigureAwait(false); + IEnumerable disks = await this.systemManagement.DiskManager.GetDisksAsync(cancellationToken); if (disks == null || !disks.Any()) { diff --git a/src/VirtualClient/VirtualClient.Contracts/Constants.cs b/src/VirtualClient/VirtualClient.Contracts/Constants.cs index 8a0f7c7021..e06572e036 100644 --- a/src/VirtualClient/VirtualClient.Contracts/Constants.cs +++ b/src/VirtualClient/VirtualClient.Contracts/Constants.cs @@ -155,6 +155,16 @@ public static class EnvironmentVariable /// public const string PATH = nameof(PATH); + /// + /// Name = SDK_EXPERIMENT_ID + /// + public const string SDK_EXPERIMENT_ID = nameof(SDK_EXPERIMENT_ID); + + /// + /// Name = SDK_METADATA + /// + public const string SDK_METADATA = nameof(SDK_METADATA); + /// /// Name = SUDO_USER /// diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/VirtualClientComponentExtensionsTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/VirtualClientComponentExtensionsTests.cs index f2bf8b2394..4d515de8fc 100644 --- a/src/VirtualClient/VirtualClient.Core.UnitTests/VirtualClientComponentExtensionsTests.cs +++ b/src/VirtualClient/VirtualClient.Core.UnitTests/VirtualClientComponentExtensionsTests.cs @@ -5,12 +5,15 @@ namespace VirtualClient { using System; using System.Collections.Generic; + using System.Collections.Specialized; using System.IO.Abstractions; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using VirtualClient.Common; + using VirtualClient.Common.Extensions; using VirtualClient.Common.Telemetry; using VirtualClient.Contracts; @@ -27,7 +30,7 @@ public void SetupDefaults(PlatformID platform) } [Test] - public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnWindowsSystems() + public async Task ExecuteCommandExtensionExecutesTheExpectedProcessOnWindowsSystems() { this.SetupDefaults(PlatformID.Win32NT); @@ -48,7 +51,7 @@ public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnWindow } [Test] - public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnWindowsSystemsWhenRunningElevated() + public async Task ExecuteCommandExtensionExecutesTheExpectedProcessOnWindowsSystemsWhenRunningElevated() { this.SetupDefaults(PlatformID.Win32NT); @@ -70,7 +73,7 @@ public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnWindow } [Test] - public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSystems() + public async Task ExecuteCommandExtensionExecutesTheExpectedProcessOnUnixSystems() { this.SetupDefaults(PlatformID.Unix); @@ -91,7 +94,7 @@ public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSy } [Test] - public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSystemsWhenRunningElevated() + public async Task ExecuteCommandExtensionExecutesTheExpectedProcessOnUnixSystemsWhenRunningElevated() { this.SetupDefaults(PlatformID.Unix); @@ -112,7 +115,7 @@ public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSy } [Test] - public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSystemsWhenRunningElevatedAndAUsernameIsSupplied() + public async Task ExecuteCommandExtensionExecutesTheExpectedProcessOnUnixSystemsWhenRunningElevatedAndAUsernameIsSupplied() { this.SetupDefaults(PlatformID.Unix); @@ -134,7 +137,7 @@ public async Task ExecuteCommandAsyncExtensionExecutesTheExpectedProcessOnUnixSy } [Test] - public void ExecuteCommandAsyncExtensionDoesNotSupportAUsernameSuppliedOnWindowsSystems() + public void ExecuteCommandExtensionDoesNotSupportAUsernameSuppliedOnWindowsSystems() { this.SetupDefaults(PlatformID.Win32NT); @@ -150,7 +153,7 @@ public void ExecuteCommandAsyncExtensionDoesNotSupportAUsernameSuppliedOnWindows } [Test] - public void ExecuteCommandAsyncExtensionDoesNotSupportAUsernameSuppliedUnlessRunningElevatedOnUnixSystems() + public void ExecuteCommandExtensionDoesNotSupportAUsernameSuppliedUnlessRunningElevatedOnUnixSystems() { this.SetupDefaults(PlatformID.Unix); @@ -164,5 +167,153 @@ public void ExecuteCommandAsyncExtensionDoesNotSupportAUsernameSuppliedUnlessRun () => component.ExecuteCommandAsync(command, commandArguments, workingDirectory, EventContext.None, CancellationToken.None, username: "notsupported")); } } + + [Test] + public void AddEnvironmentVariablesExtensionAddsExpectedEnvironmentVariablesToTheProcess_1() + { + this.SetupDefaults(PlatformID.Win32NT); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string command = "anycommand.exe"; + string commandArguments = "--option1=123 --option2=456"; + string workingDirectory = MockFixture.TestAssemblyDirectory; + + this.mockFixture.SystemManagement.Setup(s => s.ExperimentId) + .Returns(expectedExperimentId); + + using (TestExecutor component = new TestExecutor(this.mockFixture)) + { + component.Metadata.Clear(); + using (IProcessProxy process = this.mockFixture.ProcessManager.CreateProcess(command, commandArguments, workingDirectory)) + { + component.AddEnvironmentVariables(process); + Assert.IsNotNull(process.EnvironmentVariables); + Assert.IsNotEmpty(process.EnvironmentVariables); + + StringDictionary environmentVariables = process.EnvironmentVariables as StringDictionary; + Assert.IsNotNull(environmentVariables); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_EXPERIMENT_ID)); + Assert.IsFalse(environmentVariables.ContainsKey(EnvironmentVariable.SDK_METADATA)); + Assert.AreEqual(expectedExperimentId, environmentVariables[EnvironmentVariable.SDK_EXPERIMENT_ID]); + } + } + } + + [Test] + public void AddEnvironmentVariablesExtensionAddsExpectedEnvironmentVariablesToTheProcess_2() + { + this.SetupDefaults(PlatformID.Win32NT); + + string expectedExperimentId = Guid.NewGuid().ToString(); + IDictionary expectedMetadata = new Dictionary() + { + { "key1", "value1" }, + { "key2", 1234 }, + { "key3", true } + }; + + string command = "anycommand.exe"; + string commandArguments = "--option1=123 --option2=456"; + string workingDirectory = MockFixture.TestAssemblyDirectory; + + this.mockFixture.SystemManagement.Setup(s => s.ExperimentId) + .Returns(expectedExperimentId); + + using (TestExecutor component = new TestExecutor(this.mockFixture)) + { + component.Metadata.Clear(); + component.Metadata.AddRange(expectedMetadata); + + using (IProcessProxy process = this.mockFixture.ProcessManager.CreateProcess(command, commandArguments, workingDirectory)) + { + component.AddEnvironmentVariables(process); + Assert.IsNotNull(process.EnvironmentVariables); + Assert.IsNotEmpty(process.EnvironmentVariables); + + StringDictionary environmentVariables = process.EnvironmentVariables as StringDictionary; + Assert.IsNotNull(environmentVariables); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_EXPERIMENT_ID)); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_METADATA)); + Assert.AreEqual(expectedExperimentId, environmentVariables[EnvironmentVariable.SDK_EXPERIMENT_ID]); + + Assert.AreEqual( + string.Join(';', expectedMetadata.Select(entry => $"{entry.Key}={entry.Value}")), + environmentVariables[EnvironmentVariable.SDK_METADATA]); + } + } + } + + [Test] + public async Task ExecuteCommandExtensionAddsExpectedEnvironmentVariablesToTheProcess_1() + { + this.SetupDefaults(PlatformID.Win32NT); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string command = "anycommand.exe"; + string commandArguments = "--option1=123 --option2=456"; + string workingDirectory = MockFixture.TestAssemblyDirectory; + + this.mockFixture.SystemManagement.Setup(s => s.ExperimentId) + .Returns(expectedExperimentId); + + using (TestExecutor component = new TestExecutor(this.mockFixture)) + { + component.Metadata.Clear(); + using (IProcessProxy process = await component.ExecuteCommandAsync(command, commandArguments, workingDirectory, EventContext.None, CancellationToken.None)) + { + Assert.IsNotNull(process.EnvironmentVariables); + Assert.IsNotEmpty(process.EnvironmentVariables); + + StringDictionary environmentVariables = process.EnvironmentVariables as StringDictionary; + Assert.IsNotNull(environmentVariables); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_EXPERIMENT_ID)); + Assert.IsFalse(environmentVariables.ContainsKey(EnvironmentVariable.SDK_METADATA)); + Assert.AreEqual(expectedExperimentId, environmentVariables[EnvironmentVariable.SDK_EXPERIMENT_ID]); + } + } + } + + [Test] + public async Task ExecuteCommandExtensionAddsExpectedEnvironmentVariablesToTheProcess_2() + { + this.SetupDefaults(PlatformID.Win32NT); + + string expectedExperimentId = Guid.NewGuid().ToString(); + IDictionary expectedMetadata = new Dictionary() + { + { "key1", "value1" }, + { "key2", 1234 }, + { "key3", true } + }; + + string command = "anycommand.exe"; + string commandArguments = "--option1=123 --option2=456"; + string workingDirectory = MockFixture.TestAssemblyDirectory; + + this.mockFixture.SystemManagement.Setup(s => s.ExperimentId) + .Returns(expectedExperimentId); + + using (TestExecutor component = new TestExecutor(this.mockFixture)) + { + component.Metadata.Clear(); + component.Metadata.AddRange(expectedMetadata); + + using (IProcessProxy process = await component.ExecuteCommandAsync(command, commandArguments, workingDirectory, EventContext.None, CancellationToken.None)) + { + Assert.IsNotNull(process.EnvironmentVariables); + Assert.IsNotEmpty(process.EnvironmentVariables); + + StringDictionary environmentVariables = process.EnvironmentVariables as StringDictionary; + Assert.IsNotNull(environmentVariables); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_EXPERIMENT_ID)); + Assert.IsTrue(environmentVariables.ContainsKey(EnvironmentVariable.SDK_METADATA)); + Assert.AreEqual(expectedExperimentId, environmentVariables[EnvironmentVariable.SDK_EXPERIMENT_ID]); + + Assert.AreEqual( + string.Join(';', expectedMetadata.Select(entry => $"{entry.Key}={entry.Value}")), + environmentVariables[EnvironmentVariable.SDK_METADATA]); + } + } + } } } diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/WindowsDiskManagerTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/WindowsDiskManagerTests.cs index 0af55ebed6..96f6ae1d79 100644 --- a/src/VirtualClient/VirtualClient.Core.UnitTests/WindowsDiskManagerTests.cs +++ b/src/VirtualClient/VirtualClient.Core.UnitTests/WindowsDiskManagerTests.cs @@ -131,7 +131,7 @@ public async Task WindowsDiskManagerCallsTheExpectedDiskPartCommandsToCreateAMou } }; - await this.diskManager.CreateMountPointAsync(diskPartition, expectedMountPath, CancellationToken.None).ConfigureAwait(false); + await this.diskManager.CreateMountPointAsync(diskPartition, expectedMountPath, CancellationToken.None); Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); @@ -180,9 +180,9 @@ public async Task WindowsDiskManagerCallsTheExpectedDiskPartCommandsToCreateAMou } }; - await this.diskManager.CreateMountPointAsync(diskPartition, expectedMountPath, CancellationToken.None).ConfigureAwait(false); + await this.diskManager.CreateMountPointAsync(diskPartition, expectedMountPath, CancellationToken.None); - Assert.IsNotEmpty(actualCommands); + Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); CollectionAssert.AreEquivalent(expectedCommands, actualCommands); } @@ -274,7 +274,7 @@ public async Task WindowsDiskManagerCallsTheExpectedDiskPartCommandsToPartitionA } }; - await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, FileSystemType.Ntfs, CancellationToken.None).ConfigureAwait(false); + await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, FileSystemType.Ntfs, CancellationToken.None); Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); @@ -350,7 +350,7 @@ public async Task WindowsDiskManagerUsesTheExpectedFileSystemWhenFormattingAPart } }; - await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, fileSystemType, CancellationToken.None).ConfigureAwait(false); + await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, fileSystemType, CancellationToken.None); Assert.IsNotEmpty(actualCommands); Assert.IsTrue(actualCommands.Contains($"format fs={fileSystemType.ToString().ToLowerInvariant()} quick")); @@ -436,7 +436,7 @@ public async Task WindowsDiskManagerHandlesReservedPartitionsOnDisksDuringTheFor } }; - await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, FileSystemType.Ntfs, CancellationToken.None).ConfigureAwait(false); + await this.diskManager.FormatDiskAsync(diskToFormat, PartitionType.Gpt, FileSystemType.Ntfs, CancellationToken.None); Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); @@ -1662,7 +1662,7 @@ public async Task WindowsDiskManagerGetsTheExpectedDisks_Scenario1() } }; - IEnumerable actualDisks = await this.diskManager.GetDisksAsync(CancellationToken.None).ConfigureAwait(false); + IEnumerable actualDisks = await this.diskManager.GetDisksAsync(CancellationToken.None); Assert.IsNotNull(actualDisks); Assert.IsNotEmpty(actualDisks); @@ -1709,7 +1709,7 @@ public async Task WindowsDiskManagerCallsTheExpectedDiskPartCommandsToSetSanPoli } }; - await this.diskManager.SetSanPolicyAsync(CancellationToken.None).ConfigureAwait(false); + await this.diskManager.SetSanPolicyAsync(CancellationToken.None, TimeSpan.FromMilliseconds(10)); Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); @@ -1751,7 +1751,7 @@ public async Task WindowsDiskManagerSkipsSettingSanPolicyWhenItIsAlreadyOnlineAl } }; - await this.diskManager.SetSanPolicyAsync(CancellationToken.None).ConfigureAwait(false); + await this.diskManager.SetSanPolicyAsync(CancellationToken.None, TimeSpan.FromMilliseconds(10)); Assert.IsNotEmpty(actualCommands); Assert.AreEqual(expectedCommands.Count, actualCommands.Count); @@ -1767,7 +1767,7 @@ public void WindowsDiskManagerThrowsWhenSettingSanPolicyTimesOut() // Do not write any response to standard output — the WaitForResponseAsync will time out. this.standardInput.BytesWritten += (sender, data) => { }; - Assert.ThrowsAsync(() => this.diskManager.SetSanPolicyAsync(CancellationToken.None)); + Assert.ThrowsAsync(() => this.diskManager.SetSanPolicyAsync(CancellationToken.None, TimeSpan.FromMilliseconds(10))); } private class TestWindowsDiskManager : WindowsDiskManager diff --git a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand.cs b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand.cs index 66586d0101..1830a14c55 100644 --- a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand.cs +++ b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand.cs @@ -5,6 +5,7 @@ namespace VirtualClient { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -171,6 +172,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel using (IProcessProxy process = this.processManager.CreateProcess(effectiveCommand, effectiveCommandArguments, effectiveWorkingDirectory)) { + this.AddEnvironmentVariables(process); this.AddEnvironmentVariables(process, environmentVariables); await process.StartAndWaitAsync(cancellationToken); diff --git a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor.cs b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor.cs index 46e7ec27ca..8113914947 100644 --- a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor.cs +++ b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor.cs @@ -279,6 +279,7 @@ protected async Task ExecuteCommandAsync(EventContext telemetryContext, Cancella using (IProcessProxy process = this.processManager.CreateProcess(effectiveCommand, effectiveCommandArguments, effectiveWorkingDirectory)) { + this.AddEnvironmentVariables(process); this.AddEnvironmentVariables(process, environmentVariables); await process.StartAndWaitAsync(cancellationToken); diff --git a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor_Shell.cs b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor_Shell.cs index 729948372e..e746b12c9a 100644 --- a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor_Shell.cs +++ b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommandMonitor_Shell.cs @@ -57,6 +57,7 @@ protected async Task ExecuteCommandWithShellSupportAsync(EventContext telemetryC using (IProcessProxy process = this.processManager.CreateProcess(effectiveCommand, effectiveCommandArguments, effectiveWorkingDirectory)) { + this.AddEnvironmentVariables(process); this.AddEnvironmentVariables(process, environmentVariables); await process.StartAndWaitAsync(cancellationToken); diff --git a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand_Shell.cs b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand_Shell.cs index 56010d0913..d57028d013 100644 --- a/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand_Shell.cs +++ b/src/VirtualClient/VirtualClient.Core/Components/ExecuteCommand_Shell.cs @@ -57,6 +57,7 @@ protected async Task ExecuteWithShellSupportAsync(EventContext telemetryContext, using (IProcessProxy process = this.processManager.CreateProcess(effectiveCommand, effectiveCommandArguments, effectiveWorkingDirectory)) { + this.AddEnvironmentVariables(process); this.AddEnvironmentVariables(process, environmentVariables); await process.StartAndWaitAsync(cancellationToken); diff --git a/src/VirtualClient/VirtualClient.Core/DiskManager.cs b/src/VirtualClient/VirtualClient.Core/DiskManager.cs index 06cb55c54c..3ba6ca9f7c 100644 --- a/src/VirtualClient/VirtualClient.Core/DiskManager.cs +++ b/src/VirtualClient/VirtualClient.Core/DiskManager.cs @@ -47,6 +47,6 @@ protected DiskManager(ILogger logger = null) public abstract Task> GetDisksAsync(CancellationToken cancellationToken); /// - public abstract Task SetSanPolicyAsync(CancellationToken cancellationToken); + public abstract Task SetSanPolicyAsync(CancellationToken cancellationToken, TimeSpan? timeout = null); } } diff --git a/src/VirtualClient/VirtualClient.Core/IDiskManager.cs b/src/VirtualClient/VirtualClient.Core/IDiskManager.cs index ae5e360a7c..9ee272ee6a 100644 --- a/src/VirtualClient/VirtualClient.Core/IDiskManager.cs +++ b/src/VirtualClient/VirtualClient.Core/IDiskManager.cs @@ -3,6 +3,7 @@ namespace VirtualClient { + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -46,6 +47,7 @@ public interface IDiskManager /// marked read-only by the OS. This operation is a no-op on Linux. /// /// A token that can be used to cancel the operation. - Task SetSanPolicyAsync(CancellationToken cancellationToken); + /// A timeout to apply to the operation. + Task SetSanPolicyAsync(CancellationToken cancellationToken, TimeSpan? timeout = null); } } diff --git a/src/VirtualClient/VirtualClient.Core/UnixDiskManager.cs b/src/VirtualClient/VirtualClient.Core/UnixDiskManager.cs index 3359637e6d..3a9c641abc 100644 --- a/src/VirtualClient/VirtualClient.Core/UnixDiskManager.cs +++ b/src/VirtualClient/VirtualClient.Core/UnixDiskManager.cs @@ -85,7 +85,8 @@ public override Task CreateMountPointAsync(DiskVolume volume, string mountPoint, /// SAN policy is a Windows-only concept. This operation is a no-op on Linux/Unix. /// /// A token that can be used to cancel the operation. - public override Task SetSanPolicyAsync(CancellationToken cancellationToken) + /// A timeout to apply to the operation. + public override Task SetSanPolicyAsync(CancellationToken cancellationToken, TimeSpan? timeout = null) { return Task.CompletedTask; } diff --git a/src/VirtualClient/VirtualClient.Core/VirtualClientComponentExtensions.cs b/src/VirtualClient/VirtualClient.Core/VirtualClientComponentExtensions.cs index d54728ee95..1c158d7eb5 100644 --- a/src/VirtualClient/VirtualClient.Core/VirtualClientComponentExtensions.cs +++ b/src/VirtualClient/VirtualClient.Core/VirtualClientComponentExtensions.cs @@ -30,6 +30,26 @@ public static class VirtualClientComponentExtensions private static readonly IAsyncPolicy FileSystemAccessRetryPolicy = Policy.Handle() .WaitAndRetryAsync(5, (retries) => TimeSpan.FromSeconds(retries + 1)); + /// + /// Adds core/SDK environment variables to the process that is being executed. + /// + /// The component supplying the environment variable values. + /// The process to which the environment variable should be added. + public static void AddEnvironmentVariables(this VirtualClientComponent component, IProcessProxy process) + { + component.ThrowIfNull(nameof(component)); + + // SDK_EXPERIMENT_ID + process.EnvironmentVariables[EnvironmentVariable.SDK_EXPERIMENT_ID] = component.ExperimentId; + + // SDK_METADATA + if (component.Metadata?.Any() == true) + { + string metadata = string.Join(';', component.Metadata.Select(entry => $"{entry.Key}={entry.Value}")); + process.EnvironmentVariables[EnvironmentVariable.SDK_METADATA] = metadata; + } + } + /// /// Executes a command within an isolated process. /// @@ -147,6 +167,7 @@ public static async Task ExecuteCommandAsync( process = processManager.CreateElevatedProcess(component.Platform, command, commandArguments, workingDirectory, username); } + component.AddEnvironmentVariables(process); component.CleanupTasks.Add(() => process.SafeKill(component.Logger)); component.Logger.LogTraceMessage($"Executing: {command} {safeArguments}".Trim(), relatedContext); diff --git a/src/VirtualClient/VirtualClient.Core/WindowsDiskManager.cs b/src/VirtualClient/VirtualClient.Core/WindowsDiskManager.cs index 491676b431..a2526fee83 100644 --- a/src/VirtualClient/VirtualClient.Core/WindowsDiskManager.cs +++ b/src/VirtualClient/VirtualClient.Core/WindowsDiskManager.cs @@ -156,9 +156,11 @@ await process.WriteInput(command) /// online and writable rather than remaining offline or read-only. /// /// A token that can be used to cancel the operation. - public override Task SetSanPolicyAsync(CancellationToken cancellationToken) + /// A timeout to apply to the operation. + public override Task SetSanPolicyAsync(CancellationToken cancellationToken, TimeSpan? timeout = null) { EventContext context = EventContext.Persisted(); + TimeSpan effectiveTimeout = timeout ?? TimeSpan.FromSeconds(30); return this.Logger.LogMessageAsync($"{nameof(WindowsDiskManager)}.SetSanPolicy", context, async () => { @@ -185,7 +187,7 @@ await this.RetryPolicy.ExecuteAsync(async () => // Query the current SAN policy first. command = "san"; await process.WriteInput(command) - .WaitForResponseAsync(@"SAN Policy\s*:", cancellationToken, timeout: TimeSpan.FromSeconds(30)) + .WaitForResponseAsync(@"SAN Policy\s*:", cancellationToken, timeout: effectiveTimeout) .ConfigureAwait(false); string sanOutput = process.StandardOutput.ToString(); @@ -203,7 +205,7 @@ await process.WriteInput(command) // brought online and writable instead of remaining offline/read-only. command = "san policy=onlineall"; await process.WriteInput(command) - .WaitForResponseAsync(@"DiskPart successfully changed the SAN policy for the current operating system\.", cancellationToken, timeout: TimeSpan.FromSeconds(30)) + .WaitForResponseAsync(@"DiskPart successfully changed the SAN policy for the current operating system\.", cancellationToken, timeout: effectiveTimeout) .ConfigureAwait(false); this.Logger.LogTraceMessage("SAN policy set to OnlineAll.", context); @@ -219,7 +221,7 @@ await process.WriteInput(command) finally { process.WriteInput("exit"); - await Task.Delay(this.WaitTime).ConfigureAwait(false); + await Task.Delay(this.WaitTime); context.AddProcessDetails(process.ToProcessDetails("diskpart"), "diskpartProcess"); } } diff --git a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/SetDiskSanPolicyTests.cs b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/SetDiskSanPolicyTests.cs index abc3423e30..efd8eee0ef 100644 --- a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/SetDiskSanPolicyTests.cs +++ b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/SetDiskSanPolicyTests.cs @@ -28,7 +28,7 @@ public async Task SetDiskSanPolicyCallsDiskManagerSetSanPolicyOnWindows() await component.ExecuteAsync(CancellationToken.None); this.mockFixture.DiskManager.Verify( - mgr => mgr.SetSanPolicyAsync(It.IsAny()), + mgr => mgr.SetSanPolicyAsync(It.IsAny(), It.IsAny()), Times.Once); } } @@ -44,7 +44,7 @@ public async Task SetDiskSanPolicyDoesNotCallDiskManagerSetSanPolicyOnLinux() await component.ExecuteAsync(CancellationToken.None); this.mockFixture.DiskManager.Verify( - mgr => mgr.SetSanPolicyAsync(It.IsAny()), + mgr => mgr.SetSanPolicyAsync(It.IsAny(), It.IsAny()), Times.Never); } } @@ -56,7 +56,7 @@ public void SetDiskSanPolicyPropagatesExceptionsThrownByDiskManagerOnWindows() this.mockFixture.Setup(PlatformID.Win32NT); this.mockFixture.DiskManager - .Setup(mgr => mgr.SetSanPolicyAsync(It.IsAny())) + .Setup(mgr => mgr.SetSanPolicyAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(new ProcessException("DiskPart SAN policy command failed.", ErrorReason.DiskFormatFailed)); using (SetDiskSanPolicy component = new SetDiskSanPolicy(this.mockFixture.Dependencies, this.mockFixture.Parameters)) diff --git a/src/VirtualClient/VirtualClient.TestFramework/InMemoryDiskManager.cs b/src/VirtualClient/VirtualClient.TestFramework/InMemoryDiskManager.cs index 6c614cfb45..acfbd784a9 100644 --- a/src/VirtualClient/VirtualClient.TestFramework/InMemoryDiskManager.cs +++ b/src/VirtualClient/VirtualClient.TestFramework/InMemoryDiskManager.cs @@ -133,7 +133,8 @@ public Task> GetDisksAsync(CancellationToken cancellationToken /// No-op in the test/in-memory disk manager. SAN policy changes are Windows-only. /// /// A token that can be used to cancel the operation. - public Task SetSanPolicyAsync(CancellationToken cancellationToken) + /// A timeout to apply to the operation. + public Task SetSanPolicyAsync(CancellationToken cancellationToken, TimeSpan? timeout = null) { return Task.CompletedTask; }