Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
CommandBuildStep.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using SiliconStudio.Core.Diagnostics;
10 using SiliconStudio.Core.Serialization.Assets;
11 using SiliconStudio.Core.Storage;
12 using SiliconStudio.Core.IO;
13 
14 using System.Diagnostics;
15 using System.ServiceModel;
16 
17 namespace SiliconStudio.BuildEngine
18 {
19  public class CommandBuildStep : BuildStep
20  {
21  /// <inheritdoc />
22  public override string Title { get { return Command != null ? Command.Title : "<No command>"; } }
23 
24  public Command Command { get; private set; }
25 
26  /// <summary>
27  /// Command Result, set only after step completion. Not thread safe, should not be modified
28  /// </summary>
30 
31  /// <summary>
32  /// When the command is executed by another local process, a thread of the master builder will be blocked to save CPU for the slave one.
33  /// However, the slave process may spawn another command on the master, in which case we would like to unblock the thread and await for the spawned command.
34  /// </summary>
35  private readonly List<Task> spawnedCommandsToWait = new List<Task>();
36 
37  public CommandBuildStep(Command command)
38  {
39  Command = command;
40  }
41 
42  public override string ToString()
43  {
44  return Command.ToString();
45  }
46 
47  public override BuildStep Clone()
48  {
49  return new CommandBuildStep(Command.Clone());
50  }
51 
52  public override void Clean(IExecuteContext executeContext, BuilderContext builderContext, bool deleteOutput)
53  {
54  // try to retrieve result from one of the object store
55  ObjectId commandHash = Command.ComputeCommandHash(executeContext);
56 
57  var commandResultsFileStream = executeContext.ResultMap.OpenStream(commandHash, VirtualFileMode.OpenOrCreate, VirtualFileAccess.ReadWrite, VirtualFileShare.ReadWrite);
58  var commandResultEntries = new ListStore<CommandResultEntry>(commandResultsFileStream) { AutoLoadNewValues = false };
59  commandResultEntries.LoadNewValues();
60  commandResultsFileStream.Close();
61 
62  CommandResultEntry matchingResult = FindMatchingResult(executeContext, commandResultEntries.GetValues());
63  if (matchingResult != null)
64  {
65  if (deleteOutput)
66  {
67  foreach (KeyValuePair<ObjectUrl, ObjectId> outputObject in matchingResult.OutputObjects)
68  {
69  switch (outputObject.Key.Type)
70  {
71  case UrlType.File:
72  try
73  {
74  if (File.Exists(outputObject.Key.Path))
75  File.Delete(outputObject.Key.Path);
76  }
77  catch (Exception)
78  {
79  executeContext.Logger.Error("Unable to delete file: " + outputObject.Key.Path);
80  }
81  break;
82  case UrlType.Internal:
83  executeContext.ResultMap.Delete(outputObject.Value);
84  break;
85  }
86  }
87  }
88  foreach (CommandBuildStep spawnedStep in matchingResult.SpawnedCommands.Select(spawnedCommand => new CommandBuildStep(spawnedCommand)))
89  {
90  spawnedStep.Clean(executeContext, builderContext, deleteOutput);
91  }
92  }
93 
94  executeContext.ResultMap.Delete(commandHash);
95  }
96 
97  public override async Task<ResultStatus> Execute(IExecuteContext executeContext, BuilderContext builderContext)
98  {
99  ListStore<CommandResultEntry> commandResultEntries;
100 
101  // Prevent several command build step to evaluate wheither they should start at the same time. This increase the efficiency of the builder by avoiding the same command to be executed several time in parallel
102  // NOTE: Careful here, there's no try/finally block around the monitor Enter/Exit, so no non-fatal exception is allowed!
103  Monitor.Enter(executeContext);
104 
105  ObjectId commandHash;
106  //await Task.Factory.StartNew(() =>
107  {
108  // try to retrieve result from one of the object store
109  commandHash = Command.ComputeCommandHash(executeContext);
110  var commandResultsFileStream = executeContext.ResultMap.OpenStream(commandHash, VirtualFileMode.OpenOrCreate, VirtualFileAccess.ReadWrite, VirtualFileShare.ReadWrite);
111  commandResultEntries = new ListStore<CommandResultEntry>(commandResultsFileStream) { AutoLoadNewValues = false };
112  commandResultEntries.LoadNewValues();
113  }
114  //);
115 
116  // if any external input has changed since the last execution (or if we don't have a successful execution in cache, trigger the command
117  CommandResultEntry matchingResult;
118  var status = ResultStatus.NotProcessed;
119 
120  if (ShouldExecute(executeContext, commandResultEntries.GetValues(), commandHash, out matchingResult))
121  {
122  CommandBuildStep stepInProgress = executeContext.IsCommandCurrentlyRunning(commandHash);
123  if (stepInProgress != null)
124  {
125  Monitor.Exit(executeContext);
126  executeContext.Logger.Debug("Command {0} delayed because it is currently running...", Command.ToString());
127  status = (await stepInProgress.ExecutedAsync()).Status;
128  matchingResult = stepInProgress.Result;
129  }
130  else
131  {
132  executeContext.NotifyCommandBuildStepStarted(this, commandHash);
133  Monitor.Exit(executeContext);
134 
135  executeContext.Logger.Debug("Command {0} scheduled...", Command.ToString());
136 
137  status = await StartCommand(executeContext, commandResultEntries, builderContext);
138  executeContext.NotifyCommandBuildStepFinished(this, commandHash);
139  }
140  }
141  else
142  {
143  Monitor.Exit(executeContext);
144  }
145 
146  // The command has not been executed
147  if (matchingResult != null)
148  {
149  using (commandResultEntries)
150  {
151  // the command was not started because it is already up-to-date (retrieved from cache and no change in external files since last execution)
152  executeContext.Logger.Verbose("Command {0} is up-to-date, skipping...", Command.ToString());
153 
154  // Replicate triggered builds
155  Debug.Assert(SpawnedStepsList.Count == 0);
156 
157  foreach (Command spawnedCommand in matchingResult.SpawnedCommands)
158  {
159  var spawnedStep = new CommandBuildStep(spawnedCommand);
160  SpawnedStepsList.Add(spawnedStep);
161  executeContext.ScheduleBuildStep(spawnedStep);
162  }
163 
164  // Wait for all build steps to complete.
165  // TODO: Ideally, we should store and replicate the behavior of the command that spawned it
166  // (wait if it used ScheduleAndExecute, don't wait if it used RegisterSpawnedCommandWithoutScheduling)
167  await Task.WhenAll(SpawnedSteps.Select(x => x.ExecutedAsync()));
168 
169  status = ResultStatus.NotTriggeredWasSuccessful;
170 
171  RegisterCommandResult(commandResultEntries, matchingResult, status);
172  }
173  }
174 
175  return status;
176  }
177 
178  internal async Task<ResultStatus> SpawnCommand(Command command, IExecuteContext executeContext)
179  {
180  var spawnedStep = new CommandBuildStep(command);
181  SpawnedStepsList.Add(spawnedStep);
182 
183  executeContext.ScheduleBuildStep(spawnedStep);
184  var resultStatus = (await spawnedStep.ExecutedAsync()).Status;
185 
186  return resultStatus;
187  }
188 
189  private void RegisterCommandResult(ListStore<CommandResultEntry> commandResultEntries, CommandResultEntry result, ResultStatus status)
190  {
191  //foreach (var outputObject in result.OutputObjects.Where(outputObject => outputObject.Key.Type == UrlType.Internal))
192  //{
193  // builderContext.AssetIndexMap[outputObject.Key.Path] = outputObject.Value;
194  //}
195 
196  Result = result;
197 
198  // Only save to build cache if compilation was done and successful
199  if (status == ResultStatus.Successful)
200  {
201  commandResultEntries.AddValue(result);
202  }
203  }
204 
205  internal bool ShouldExecute(IExecuteContext executeContext, CommandResultEntry[] previousResultCollection, ObjectId commandHash, out CommandResultEntry matchingResult)
206  {
207  IndexFileCommand.MountDatabases(executeContext);
208  try
209  {
210  matchingResult = FindMatchingResult(executeContext, previousResultCollection);
211  }
212  finally
213  {
214  IndexFileCommand.UnmountDatabases(executeContext);
215  }
216 
217  if (matchingResult == null || Command.ShouldForceExecution())
218  {
219  // Ensure we ignore existing results if the execution is forced
220  matchingResult = null;
221  return true;
222  }
223 
224  return false;
225  }
226 
227  internal CommandResultEntry FindMatchingResult(IPrepareContext prepareContext, CommandResultEntry[] commandResultCollection)
228  {
229  if (commandResultCollection == null)
230  return null;
231 
232  // Then check input dependencies and output versions
233  //builderContext.AssetIndexMap.LoadNewValues();
234 
235  foreach (CommandResultEntry entry in commandResultCollection)
236  {
237  bool entryMatch = true;
238 
239  foreach (var inputDepVersion in entry.InputDependencyVersions)
240  {
241  var hash = prepareContext.ComputeInputHash(inputDepVersion.Key.Type, inputDepVersion.Key.Path);
242  if (hash != inputDepVersion.Value)
243  {
244  entryMatch = false;
245  break;
246  }
247  }
248 
249  if (!entryMatch)
250  continue;
251 
252  if (entry.OutputObjects.Any(outputObject => !VirtualFileSystem.FileExists(FileOdbBackend.BuildUrl("/data/db", outputObject.Value))))
253  {
254  entryMatch = false;
255  }
256 
257  if (!entryMatch)
258  continue;
259 
260  // TODO/Benlitz: check matching spawned commands if needed
261 
262  return entry;
263  }
264 
265  return null;
266  }
267 
268  private async Task<ResultStatus> StartCommand(IExecuteContext executeContext, ListStore<CommandResultEntry> commandResultEntries, BuilderContext builderContext)
269  {
270  var logger = executeContext.Logger;
271 
272  // Register the cancel callback
273  var cancellationTokenSource = executeContext.CancellationTokenSource;
274  cancellationTokenSource.Token.Register(x => ((Command)x).Cancel(), Command);
275 
276  Command.CancellationToken = cancellationTokenSource.Token;
277 
278  //await Scheduler.Yield();
279 
280  ResultStatus status;
281 
282  using (commandResultEntries)
283  {
284  logger.Debug("Starting command {0}...", Command.ToString());
285 
286  // Creating the CommandResult object
287  var commandContext = new LocalCommandContext(executeContext, this, builderContext);
288 
289  // Actually processing the command
290  if (Command.ShouldSpawnNewProcess() && builderContext.MaxParallelProcesses > 0)
291  {
292  while (!builderContext.CanSpawnParallelProcess())
293  {
294  await Task.Delay(1, Command.CancellationToken);
295  }
296 
297  var address = "net.pipe://localhost/" + Guid.NewGuid();
298  var arguments = string.Format("--slave=\"{0}\" --build-path=\"{1}\" --profile=\"{2}\"", address, builderContext.BuildPath, builderContext.BuildProfile);
299 
300  var startInfo = new ProcessStartInfo
301  {
302  FileName = builderContext.SlaveBuilderPath,
303  Arguments = arguments,
304  WorkingDirectory = Environment.CurrentDirectory,
305  CreateNoWindow = true,
306  UseShellExecute = false,
307  RedirectStandardOutput = true,
308  RedirectStandardError = true,
309  };
310 
311  // Start WCF pipe for communication with process
312  var processBuilderRemote = new ProcessBuilderRemote(commandContext, Command, builderContext.Parameters);
313  var host = new ServiceHost(processBuilderRemote);
314  host.AddServiceEndpoint(typeof(IProcessBuilderRemote), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None) { MaxReceivedMessageSize = int.MaxValue }, address);
315  host.Open();
316 
317  var output = new List<string>();
318 
319  var process = new Process { StartInfo = startInfo };
320  process.Start();
321  process.OutputDataReceived += (_, args) => LockProcessAndAddDataToList(process, output, args);
322  process.ErrorDataReceived += (_, args) => LockProcessAndAddDataToList(process, output, args);
323  process.BeginOutputReadLine();
324  process.BeginErrorReadLine();
325 
326  // Attach debugger to newly created process
327  // Add a reference to EnvDTE80 in the csproj and uncomment this (and also the Thread.Sleep in BuildEngineCmmands), then start the master process without debugger to attach to a slave.
328  //var dte = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.11.0");
329  //foreach (EnvDTE.Process dteProcess in dte.Debugger.LocalProcesses)
330  //{
331  // if (dteProcess.ProcessID == process.Id)
332  // {
333  // dteProcess.Attach();
334  // dte.Debugger.CurrentProcess = dteProcess;
335  // }
336  //}
337 
338  Task[] tasksToWait = null;
339 
340  while (!process.HasExited)
341  {
342  Thread.Sleep(1);
343  lock (spawnedCommandsToWait)
344  {
345  if (spawnedCommandsToWait.Count > 0)
346  {
347  tasksToWait = spawnedCommandsToWait.ToArray();
348  spawnedCommandsToWait.Clear();
349  }
350  }
351 
352  if (tasksToWait != null)
353  {
354  await Task.WhenAll(tasksToWait);
355  tasksToWait = null;
356  }
357  }
358  host.Close();
359 
360  builderContext.NotifyParallelProcessEnded();
361 
362  if (process.ExitCode != 0)
363  {
364  logger.Debug("Remote command crashed with output:\n{0}", string.Join(Environment.NewLine, output));
365  }
366 
367  if (processBuilderRemote.Result != null)
368  {
369  // Register results back locally
370  foreach (var outputObject in processBuilderRemote.Result.OutputObjects)
371  {
372  commandContext.RegisterOutput(outputObject.Key, outputObject.Value);
373  }
374 
375  // Register tags
376  foreach (var tag in processBuilderRemote.Result.TagSymbols)
377  {
378  TagSymbol tagSymbol;
379 
380  // Resolve tag locally
381  if (!Command.TagSymbols.TryGetValue(tag.Value, out tagSymbol))
382  {
383  // Should we ignore silently? (with warning)
384  throw new InvalidOperationException("Could not find tag symbol.");
385  }
386 
387  commandContext.AddTag(tag.Key, tagSymbol);
388  }
389  }
390 
391  status = Command.CancellationToken.IsCancellationRequested ? ResultStatus.Cancelled : (process.ExitCode == 0 ? ResultStatus.Successful : ResultStatus.Failed);
392  }
393  else
394  {
395  Command.PreCommand(commandContext);
396  if (!Command.BasePreCommandCalled)
397  throw new InvalidOperationException("base.PreCommand not called in command " + Command);
398 
399  status = await Command.DoCommand(commandContext);
400 
401  Command.PostCommand(commandContext, status);
402  if (!Command.BasePostCommandCalled)
403  throw new InvalidOperationException("base.PostCommand not called in command " + Command);
404  }
405 
406  // Ensure the command set at least the result status
407  if (status == ResultStatus.NotProcessed)
408  throw new InvalidDataException("The command " + Command + " returned ResultStatus.NotProcessed after completion.");
409 
410  // Registering the result to the build cache
411  RegisterCommandResult(commandResultEntries, commandContext.ResultEntry, status);
412  }
413 
414  return status;
415  }
416 
417  private static void LockProcessAndAddDataToList(Process process, List<string> output, DataReceivedEventArgs args)
418  {
419  if (!string.IsNullOrEmpty(args.Data))
420  {
421  lock (process)
422  {
423  output.Add(args.Data);
424  }
425  }
426  }
427 
429  {
430  lock (spawnedCommandsToWait)
431  {
432  spawnedCommandsToWait.Add(task);
433  }
434  }
435  }
436 }
Virtual abstraction over a file system. It handles access to files, http, packages, path rewrite, etc...
CommandResultEntry Result
Command Result, set only after step completion. Not thread safe, should not be modified ...
static string BuildUrl(string vfsRootUrl, ObjectId objectId)
static bool FileExists(string path)
Checks the existence of a file.
void AwaitSpawnedCommand(Task< ResultStatus > task)
ResultStatus
Status of a command.
Definition: ResultStatus.cs:8
Object Database Backend (ODB) implementation using VirtualFileSystem
System.IO.File File
override void Clean(IExecuteContext executeContext, BuilderContext builderContext, bool deleteOutput)
Clean the build, deleting the command cache which is used to determine wheither a command has already...
A hash to uniquely identify data.
Definition: ObjectId.cs:13
List< Command > SpawnedCommands
Commands created during the execution of the current command.
override async Task< ResultStatus > Execute(IExecuteContext executeContext, BuilderContext builderContext)
Execute the BuildStep, usually resulting in scheduling tasks in the scheduler
override BuildStep Clone()
Clone this Build Step.