1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ namespace VirtualClient . Actions
5+ {
6+ using System ;
7+ using System . Collections . Generic ;
8+ using System . IO ;
9+ using System . IO . Abstractions ;
10+ using System . Linq ;
11+ using System . Text ;
12+ using System . Threading ;
13+ using System . Threading . Tasks ;
14+ using Microsoft . Extensions . DependencyInjection ;
15+ using MimeMapping ;
16+ using Polly ;
17+ using VirtualClient . Common ;
18+ using VirtualClient . Common . Extensions ;
19+ using VirtualClient . Common . Telemetry ;
20+ using VirtualClient . Contracts ;
21+ using VirtualClient . Contracts . Metadata ;
22+
23+ /// <summary>
24+ /// A generic command executor with file upload and metrics upload support.
25+ /// </summary>
26+ [ SupportedPlatforms ( "linux-arm64,linux-x64,win-arm64,win-x64" ) ]
27+ public class CommandExecutor : ExecuteCommand
28+ {
29+ private IFileSystem fileSystem ;
30+ private ISystemManagement systemManagement ;
31+
32+ /// <summary>
33+ /// Constructor for <see cref="ScriptExecutor"/>
34+ /// </summary>
35+ /// <param name="dependencies">Provides required dependencies to the component.</param>
36+ /// <param name="parameters">Parameters defined in the profile or supplied on the command line.</param>
37+ public CommandExecutor ( IServiceCollection dependencies , IDictionary < string , IConvertible > parameters )
38+ : base ( dependencies , parameters )
39+ {
40+ this . systemManagement = this . Dependencies . GetService < ISystemManagement > ( ) ;
41+ this . fileSystem = this . systemManagement . FileSystem ;
42+ }
43+
44+ /// <summary>
45+ /// The Regex Based, semi-colon separated relative log file/Folder Paths
46+ /// </summary>
47+ public IEnumerable < string > LogPaths
48+ {
49+ get
50+ {
51+ this . Parameters . TryGetCollection < string > ( nameof ( this . LogPaths ) , out IEnumerable < string > logPaths ) ;
52+ return logPaths ;
53+ }
54+ }
55+
56+ /// <summary>
57+ /// The ToolName for better logging and metadata
58+ /// </summary>
59+ public string ToolName
60+ {
61+ get
62+ {
63+ return this . Parameters . GetValue < string > ( nameof ( this . ToolName ) , string . Empty ) ;
64+ }
65+
66+ set
67+ {
68+ this . Parameters [ nameof ( this . ToolName ) ] = value ;
69+ }
70+ }
71+
72+ /// <summary>
73+ /// A retry policy to apply to file access/move operations.
74+ /// </summary>
75+ protected IAsyncPolicy FileOperationsRetryPolicy { get ; set ; } = RetryPolicies . FileOperations ;
76+
77+ /// <summary>
78+ /// Executes the workload.
79+ /// </summary>
80+ protected override async Task ExecuteAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
81+ {
82+ DateTime startTime = DateTime . UtcNow ;
83+
84+ try
85+ {
86+ await base . ExecuteAsync ( telemetryContext , cancellationToken ) ;
87+ }
88+ finally
89+ {
90+ DateTime endTime = DateTime . UtcNow ;
91+ await this . CaptureMetricsAsync ( startTime , endTime , telemetryContext , cancellationToken ) ;
92+ await this . CaptureLogsAsync ( cancellationToken ) ;
93+ }
94+ }
95+
96+ /// <summary>
97+ /// Captures the workload logs and the Workload metrics.
98+ /// </summary>
99+ protected Task CaptureMetricsAsync ( DateTime startTime , DateTime endTime , EventContext telemetryContext , CancellationToken cancellationToken )
100+ {
101+ this . MetadataContract . AddForScenario (
102+ this . Scenario ,
103+ SensitiveData . ObscureSecrets ( this . Command ) ) ;
104+
105+ this . MetadataContract . Apply ( telemetryContext ) ;
106+
107+ bool metricsFileFound = false ;
108+
109+ try
110+ {
111+ ////if (this.fileSystem.File.Exists(this.MetricsFilePath))
112+ ////{
113+ //// metricsFileFound = true;
114+ //// telemetryContext.AddContext("metricsFilePath", this.MetricsFilePath);
115+ //// string results = await this.fileSystem.File.ReadAllTextAsync(this.MetricsFilePath);
116+
117+ //// if (!string.IsNullOrWhiteSpace(results))
118+ //// {
119+ //// JsonMetricsParser parser = new JsonMetricsParser(results, this.Logger, telemetryContext);
120+ //// IList<Metric> workloadMetrics = parser.Parse();
121+
122+ //// this.Logger.LogMetrics(
123+ //// this.ToolName,
124+ //// (this.MetricScenario ?? this.Scenario) ?? "Script",
125+ //// process.StartTime,
126+ //// process.ExitTime,
127+ //// workloadMetrics,
128+ //// null,
129+ //// process.FullCommand(),
130+ //// this.Tags,
131+ //// telemetryContext);
132+ //// }
133+ ////}
134+ }
135+ finally
136+ {
137+ telemetryContext . AddContext ( nameof ( metricsFileFound ) , metricsFileFound ) ;
138+ }
139+
140+ return Task . CompletedTask ;
141+ }
142+
143+ /// <summary>
144+ /// Captures the workload logs based on LogFiles parameter of ScriptExecutor.
145+ /// All the files inmatching sub-folders and all the matching files along with metrics file will be moved to the
146+ /// central Virtual Client logs directory.
147+ /// </summary>
148+ protected Task CaptureLogsAsync ( CancellationToken cancellationToken )
149+ {
150+ // e.g.
151+ // /logs/anytool/executecustomscript1
152+ // /logs/anytool/executecustomscript2
153+ string destinitionLogsDir = this . Combine ( this . GetLogDirectory ( this . ToolName ) , DateTime . UtcNow . ToString ( "yyyy-MM-dd_hh-mm-ss" ) ) ;
154+
155+ IEnumerable < string > files = this . fileSystem . Directory . EnumerateFiles ( this . GetLogDirectory ( ) , "*.*" , SearchOption . AllDirectories ) ;
156+
157+ ////if (this.LogPaths?.Any() == true)
158+ ////{
159+ //// foreach (string logPath in this.LogPaths)
160+ //// {
161+ //// if (string.IsNullOrWhiteSpace(logPath))
162+ //// {
163+ //// continue;
164+ //// }
165+
166+ //// string fullLogPath = this.fileSystem.Path.GetFullPath(this.fileSystem.Path.Combine(this.ExecutableDirectory, logPath));
167+
168+ //// // Check for Matching Sub-Directories
169+ //// if (this.fileSystem.Directory.Exists(fullLogPath))
170+ //// {
171+ //// foreach (string logFilePath in this.fileSystem.Directory.GetFiles(fullLogPath, "*", SearchOption.AllDirectories))
172+ //// {
173+ //// var logs = await this.MoveLogsAsync(logFilePath, destinitionLogsDir, cancellationToken, sourceRootDirectory: fullLogPath);
174+ //// await this.RequestLogUploadsAsync(logs);
175+ //// }
176+ //// }
177+
178+ //// // Check for Matching FileNames
179+ //// foreach (string logFilePath in this.fileSystem.Directory.GetFiles(this.ExecutableDirectory, logPath, SearchOption.AllDirectories))
180+ //// {
181+ //// var logs = await this.MoveLogsAsync(logFilePath, destinitionLogsDir, cancellationToken);
182+ //// await this.RequestLogUploadsAsync(logs);
183+ //// }
184+ //// }
185+ ////}
186+
187+ ////// Move test-metrics.json file if that exists
188+ ////string metricsFilePath = this.Combine(this.ExecutableDirectory, "test-metrics.json");
189+ ////if (this.fileSystem.File.Exists(metricsFilePath))
190+ ////{
191+ //// var logs = await this.MoveLogsAsync(metricsFilePath, destinitionLogsDir, cancellationToken);
192+ //// await this.RequestLogUploadsAsync(logs);
193+ ////}
194+ return Task . CompletedTask ;
195+ }
196+
197+ /// <summary>
198+ /// Requests a file upload for each of the log file paths provided.
199+ /// </summary>
200+ protected async Task RequestLogUploadsAsync ( IEnumerable < string > logPaths )
201+ {
202+ if ( logPaths ? . Any ( ) == true && this . TryGetContentStoreManager ( out IBlobManager blobManager ) )
203+ {
204+ foreach ( string logPath in logPaths )
205+ {
206+ FileUploadDescriptor descriptor = this . CreateFileUploadDescriptor (
207+ new FileContext (
208+ this . fileSystem . FileInfo . New ( logPath ) ,
209+ MimeUtility . GetMimeMapping ( logPath ) ,
210+ Encoding . UTF8 . WebName ,
211+ this . ExperimentId ,
212+ this . AgentId ,
213+ this . ToolName ,
214+ this . Scenario ,
215+ null ,
216+ this . Roles ? . FirstOrDefault ( ) ) ) ;
217+
218+ await this . RequestFileUploadAsync ( descriptor ) ;
219+ }
220+ }
221+ }
222+
223+ /////// <summary>
224+ /////// Move the log files to central logs directory (retaining source directory structure) and Upload to Content Store.
225+ /////// </summary>
226+ ////private async Task<IEnumerable<string>> MoveLogsAsync(string sourcePath, string destinationDirectory, CancellationToken cancellationToken, string sourceRootDirectory = null)
227+ ////{
228+ //// List<string> targetLogs = new List<string>();
229+ //// if (!string.Equals(sourcePath, this.ExecutablePath))
230+ //// {
231+ //// if (!this.fileSystem.Directory.Exists(destinationDirectory))
232+ //// {
233+ //// this.fileSystem.Directory.CreateDirectory(destinationDirectory);
234+ //// }
235+
236+ //// string destPath = sourcePath;
237+ //// await (this.FileOperationsRetryPolicy ?? Policy.NoOpAsync()).ExecuteAsync(async () =>
238+ //// {
239+ //// await Task.Run(() =>
240+ //// {
241+ //// string fileName = Path.GetFileName(sourcePath);
242+
243+ //// if (!string.IsNullOrEmpty(sourceRootDirectory))
244+ //// {
245+ //// // Compute relative path from sourceRoot to sourcePath
246+ //// string relativePath = this.fileSystem.Path.GetRelativePath(sourceRootDirectory, sourcePath);
247+ //// string destDir = this.fileSystem.Path.Combine(destinationDirectory, this.fileSystem.Path.GetDirectoryName(relativePath));
248+
249+ //// if (!this.fileSystem.Directory.Exists(destDir))
250+ //// {
251+ //// this.fileSystem.Directory.CreateDirectory(destDir);
252+ //// }
253+
254+ //// destPath = this.Combine(destDir, fileName);
255+ //// }
256+ //// else
257+ //// {
258+ //// destPath = this.Combine(destinationDirectory, fileName);
259+ //// }
260+
261+ //// this.fileSystem.File.Move(sourcePath, destPath, true);
262+ //// targetLogs.Add(destPath);
263+ //// });
264+ //// });
265+ //// }
266+
267+ //// return targetLogs;
268+ ////}
269+ }
270+ }
0 commit comments