/****************************************************************************
|
**
|
** Copyright (C) 2021 The Qt Company Ltd.
|
** Contact: https://www.qt.io/licensing/
|
**
|
** This file is part of the Qt VS Tools.
|
**
|
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
** Commercial License Usage
|
** Licensees holding valid commercial Qt licenses may use this file in
|
** accordance with the commercial license agreement provided with the
|
** Software or, alternatively, in accordance with the terms contained in
|
** a written agreement between you and The Qt Company. For licensing terms
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
** information use the contact form at https://www.qt.io/contact-us.
|
**
|
** GNU General Public License Usage
|
** Alternatively, this file may be used under the terms of the GNU
|
** General Public License version 3 as published by the Free Software
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
** included in the packaging of this file. Please review the following
|
** information to ensure the GNU General Public License requirements will
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
**
|
** $QT_END_LICENSE$
|
**
|
****************************************************************************/
|
|
#region Task TaskName="QtRunWork"
|
|
#region Reference
|
#endregion
|
|
#region Using
|
using System;
|
using System.Diagnostics;
|
using System.Linq;
|
using System.Collections.Generic;
|
using Microsoft.Build.Framework;
|
using Microsoft.Build.Utilities;
|
#endregion
|
|
#region Comment
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/// TASK QtRunWork
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
// Run work items in parallel processes.
|
// Parameters:
|
// in ITaskItem[] QtWork: work items
|
// in int QtMaxProcs: maximum number of processes to run in parallel
|
// in bool QtDebug: generate debug messages
|
// out ITaskItem[] Result: list of new items with the result of each work item
|
#endregion
|
|
namespace QtVsTools.QtMsBuild.Tasks
|
{
|
public static class QtRunWork
|
{
|
public static QtMSBuild.ITaskLoggingHelper Log { get; set; }
|
|
public static bool Execute(
|
#region Parameters
|
Microsoft.Build.Framework.ITaskItem[] QtWork,
|
System.Int32 QtMaxProcs,
|
System.Boolean QtDebug,
|
out Microsoft.Build.Framework.ITaskItem[] Result
|
)
|
#endregion
|
{
|
#region Code
|
Result = new ITaskItem[] { };
|
bool ok = true;
|
var Comparer = StringComparer.InvariantCultureIgnoreCase;
|
var Comparison = StringComparison.InvariantCultureIgnoreCase;
|
|
// Work item key = "%(WorkType)(%(Identity))"
|
Func<string, string, string> KeyString = (x, y) => string.Format("{0}{{{1}}}", x, y);
|
Func<ITaskItem, string> Key = (item) =>
|
KeyString(item.GetMetadata("WorkType"), item.ItemSpec);
|
var workItemKeys = new HashSet<string>(QtWork.Select(x => Key(x)), Comparer);
|
|
// Work items, indexed by %(Identity)
|
var workItemsByIdentity = QtWork
|
.GroupBy(x => x.ItemSpec, x => Key(x), Comparer)
|
.ToDictionary(x => x.Key, x => new List<string>(x), Comparer);
|
|
// Work items, indexed by work item key
|
var workItems = QtWork.Select(x => new
|
{
|
Self = x,
|
Key = Key(x),
|
ToolPath = x.GetMetadata("ToolPath"),
|
Message = x.GetMetadata("Message"),
|
DependsOn = new HashSet<string>(comparer: Comparer,
|
collection: x.GetMetadata("DependsOn")
|
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
|
.Where(y => workItemsByIdentity.ContainsKey(y))
|
.SelectMany(y => workItemsByIdentity[y])
|
.Union(x.GetMetadata("DependsOnWork")
|
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
|
.Select(y => KeyString(y, x.ItemSpec))
|
.Where(y => workItemKeys.Contains(y)))
|
.GroupBy(y => y, Comparer).Select(y => y.Key)
|
.Where(y => !y.Equals(Key(x), Comparison))),
|
ProcessStartInfo = new ProcessStartInfo
|
{
|
FileName = x.GetMetadata("ToolPath"),
|
Arguments = x.GetMetadata("Options"),
|
CreateNoWindow = true,
|
UseShellExecute = false,
|
RedirectStandardError = true,
|
RedirectStandardOutput = true,
|
},
|
})
|
// In case of items with duplicate keys, use only the first one
|
.GroupBy(x => x.Key, Comparer)
|
.ToDictionary(x => x.Key, x => x.First(), Comparer);
|
|
// Result
|
var result = workItems.Values
|
.ToDictionary(x => x.Key, x => new TaskItem(x.Self));
|
|
// Dependency relation [item -> dependent items]
|
var dependentsOf = workItems.Values
|
.Where(x => x.DependsOn.Any())
|
.SelectMany(x => x.DependsOn.Select(y => new { Dependent = x.Key, Dependency = y }))
|
.GroupBy(x => x.Dependency, x => x.Dependent, Comparer)
|
.ToDictionary(x => x.Key, x => new List<string>(x), Comparer);
|
|
// Work items that are ready to start; initially queue all independent items
|
var workQueue = new Queue<string>(workItems.Values
|
.Where(x => !x.DependsOn.Any())
|
.Select(x => x.Key));
|
|
if (QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork queueing\r\n## {0}",
|
string.Join("\r\n## ", workQueue)));
|
}
|
|
// Postponed items; save dependent items to queue later when ready
|
var postponedItems = new HashSet<string>(workItems.Values
|
.Where(x => x.DependsOn.Any())
|
.Select(x => x.Key));
|
|
if (QtDebug && postponedItems.Any()) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork postponed dependents\r\n## {0}",
|
string.Join("\r\n## ", postponedItems
|
.Select(x => string.Format("{0} <- {1}", x,
|
string.Join(", ", workItems[x].DependsOn))))));
|
}
|
|
// Work items that are running; must synchronize with the exit of all processes
|
var running = new Queue<KeyValuePair<string, Process>>();
|
|
// Work items that have terminated
|
var terminated = new HashSet<string>(Comparer);
|
|
// While there are work items queued, start a process for each item
|
while (ok && workQueue.Any()) {
|
|
var workItem = workItems[workQueue.Dequeue()];
|
Log.LogMessage(MessageImportance.High, workItem.Message);
|
|
try {
|
var proc = Process.Start(workItem.ProcessStartInfo);
|
proc.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
|
{
|
if (!string.IsNullOrEmpty(e.Data))
|
Log.LogMessage(MessageImportance.High, string.Join(" ", new[]
|
{
|
(QtDebug ? "[" + (((Process)sender).Id.ToString()) + "]" : ""),
|
e.Data
|
}));
|
};
|
proc.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
|
{
|
if (!string.IsNullOrEmpty(e.Data))
|
Log.LogMessage(MessageImportance.High, string.Join(" ", new[]
|
{
|
(QtDebug ? "[" + (((Process)sender).Id.ToString()) + "]" : ""),
|
e.Data
|
}));
|
};
|
proc.BeginOutputReadLine();
|
proc.BeginErrorReadLine();
|
running.Enqueue(new KeyValuePair<string, Process>(workItem.Key, proc));
|
} catch (Exception e) {
|
Log.LogError(
|
string.Format("[QtRunWork] Error starting process {0}: {1}",
|
workItem.ToolPath, e.Message));
|
ok = false;
|
}
|
|
string qtDebugRunning = "";
|
if (QtDebug) {
|
qtDebugRunning = string.Format("## QtRunWork waiting {0}",
|
string.Join(", ", running
|
.Select(x => string.Format("{0} [{1}]", x.Key, x.Value.Id))));
|
}
|
|
// Wait for process to terminate when there are processes running, and...
|
while (ok && running.Any()
|
// ...work is queued but already reached the maximum number of processes, or...
|
&& ((workQueue.Any() && running.Count >= QtMaxProcs)
|
// ...work queue is empty but there are dependents that haven't yet been queued
|
|| (!workQueue.Any() && postponedItems.Any()))) {
|
|
var itemProc = running.Dequeue();
|
workItem = workItems[itemProc.Key];
|
var proc = itemProc.Value;
|
|
if (QtDebug && !string.IsNullOrEmpty(qtDebugRunning)) {
|
Log.LogMessage(MessageImportance.High, qtDebugRunning);
|
qtDebugRunning = "";
|
}
|
|
if (proc.WaitForExit(100)) {
|
if (QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork exit {0} [{1}] = {2} ({3:0.00} msecs)",
|
workItem.Key, proc.Id, proc.ExitCode,
|
(proc.ExitTime - proc.StartTime).TotalMilliseconds));
|
}
|
|
// Process terminated; check exit code and close
|
terminated.Add(workItem.Key);
|
result[workItem.Key].SetMetadata("ExitCode", proc.ExitCode.ToString());
|
ok &= (proc.ExitCode == 0);
|
proc.Close();
|
|
// Add postponed dependent items to work queue
|
if (ok && dependentsOf.ContainsKey(workItem.Key)) {
|
// Dependents of workItem...
|
var readyDependents = dependentsOf[workItem.Key]
|
// ...that have not yet been queued...
|
.Where(x => postponedItems.Contains(x)
|
// ...and whose dependending items have all terminated.
|
&& workItems[x].DependsOn.All(y => terminated.Contains(y)));
|
|
if (QtDebug && readyDependents.Any()) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork queueing\r\n## {0}",
|
string.Join("\r\n## ", readyDependents)));
|
}
|
|
foreach (var dependent in readyDependents) {
|
postponedItems.Remove(dependent);
|
workQueue.Enqueue(dependent);
|
}
|
}
|
} else {
|
// Process is still running; feed it back into the running queue
|
running.Enqueue(itemProc);
|
}
|
}
|
}
|
|
// If there are items still haven't been queued, that means a circular dependency exists
|
if (ok && postponedItems.Any()) {
|
ok = false;
|
Log.LogError("[QtRunWork] Error: circular dependency");
|
if (QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork circularity\r\n## {0}",
|
string.Join("\r\n## ", postponedItems
|
.Select(x => string.Format("{0} <- {1}", x,
|
string.Join(", ", workItems[x].DependsOn))))));
|
}
|
}
|
|
if (ok && QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
"## QtRunWork all work queued");
|
if (running.Any()) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork waiting {0}",
|
string.Join(", ", running
|
.Select(x => string.Format("{0} [{1}]", x.Key, x.Value.Id)))));
|
}
|
}
|
|
// Wait for all running processes to terminate
|
while (running.Any()) {
|
var itemProc = running.Dequeue();
|
var workItem = workItems[itemProc.Key];
|
var proc = itemProc.Value;
|
if (proc.WaitForExit(100)) {
|
if (QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork exit {0} [{1}] = {2} ({3:0.00} msecs)",
|
workItem.Key, proc.Id, proc.ExitCode,
|
(proc.ExitTime - proc.StartTime).TotalMilliseconds));
|
}
|
// Process terminated; check exit code and close
|
result[workItem.Key].SetMetadata("ExitCode", proc.ExitCode.ToString());
|
ok &= (proc.ExitCode == 0);
|
proc.Close();
|
} else {
|
// Process is still running; feed it back into the running queue
|
running.Enqueue(itemProc);
|
}
|
}
|
|
if (QtDebug) {
|
Log.LogMessage(MessageImportance.High,
|
string.Format("## QtRunWork result {0}", (ok ? "ok" : "FAILED!")));
|
}
|
|
Result = result.Values.ToArray();
|
if (!ok)
|
return false;
|
#endregion
|
|
return true;
|
}
|
}
|
}
|
#endregion
|