/**************************************************************************** ** ** Copyright (C) 2019 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$ ** ****************************************************************************/ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Properties; using Microsoft.VisualStudio.TaskStatusCenter; using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.VCProjectEngine; namespace QtVsTools.QtMsBuild { using Core; using VisualStudio; using Microsoft.VisualStudio.Shell; using Thread = System.Threading.Thread; using Task = System.Threading.Tasks.Task; using SubscriberAction = ActionBlock>; class QtProjectTracker : Concurrent { static ConcurrentDictionary _Instances; static ConcurrentDictionary Instances => StaticThreadSafeInit(() => _Instances, () => _Instances = new ConcurrentDictionary()); static PunisherQueue _InitQueue; static PunisherQueue InitQueue => StaticThreadSafeInit(() => _InitQueue, () => _InitQueue = new PunisherQueue()); static IVsTaskStatusCenterService _StatusCenter; static IVsTaskStatusCenterService StatusCenter => StaticThreadSafeInit(() => _StatusCenter, () => _StatusCenter = VsServiceProvider .GetService()); static Task InitDispatcher { get; set; } static ITaskHandler2 InitStatus { get; set; } public static string SolutionPath { get; set; } = string.Empty; private QtProjectTracker() { Initialized = new EventWaitHandle(false, EventResetMode.ManualReset); } class Subscriber : IDisposable { public Subscriber(QtProjectTracker tracker, ConfiguredProject config) { Tracker = tracker; Config = config; Subscription = Config.Services.ProjectSubscription.JointRuleSource.SourceBlock .LinkTo(new SubscriberAction(ProjectUpdateAsync), ruleNames: new[] { "ClCompile", "QtRule10_Settings", "QtRule30_Moc", "QtRule40_Rcc", "QtRule60_Repc", "QtRule50_Uic", "QtRule_Translation", "QtRule70_Deploy", }, initialDataAsNew: false ); } QtProjectTracker Tracker { get; } ConfiguredProject Config { get; } IDisposable Subscription { get; set; } public void Dispose() { Subscription?.Dispose(); Subscription = null; } async Task ProjectUpdateAsync(IProjectVersionedValue update) { await Tracker.OnProjectUpdateAsync(Config, update.Value); } } public EnvDTE.Project Project { get; private set; } public VCProject VcProject { get; private set; } public UnconfiguredProject UnconfiguredProject { get; private set; } public EventWaitHandle Initialized { get; } List Subscribers { get; set; } public static bool IsTracked(string projectPath) { return Instances.ContainsKey(projectPath); } public static void Add(EnvDTE.Project project) { if (!QtVsToolsPackage.Instance.Options.ProjectTracking) return; ThreadHelper.ThrowIfNotOnUIThread(); Get(project, project.FullName); } public static QtProjectTracker Get(EnvDTE.Project project, string projectPath) { lock (StaticCriticalSection) { if (Instances.TryGetValue(projectPath, out QtProjectTracker tracker)) return tracker; tracker = new QtProjectTracker { Project = project, }; Instances[projectPath] = tracker; InitQueue.Enqueue(tracker); if (InitDispatcher == null) InitDispatcher = Task.Run(InitDispatcherLoopAsync); return tracker; } } public static void Reset() { lock (StaticCriticalSection) { Instances.Clear(); InitQueue.Clear(); } } static async Task InitDispatcherLoopAsync() { while (!QtVsToolsPackage.Instance.Zombied) { while (InitQueue.IsEmpty) await Task.Delay(100); if (InitQueue.TryDequeue(out QtProjectTracker tracker)) { if (InitStatus == null) { await QtVsToolsPackage.Instance.JoinableTaskFactory .SwitchToMainThreadAsync(); tracker.BeginInitStatus(); await TaskScheduler.Default; } else { await QtVsToolsPackage.Instance.JoinableTaskFactory .SwitchToMainThreadAsync(); tracker.UpdateInitStatus(0); await TaskScheduler.Default; } await tracker.InitializeAsync(); } if (InitStatus != null && (InitQueue.IsEmpty || InitStatus.UserCancellation.IsCancellationRequested)) { if (InitStatus.UserCancellation.IsCancellationRequested) { InitQueue.Clear(); } tracker.EndInitStatus(); } } } async Task InitializeAsync() { int p = 0; UpdateInitStatus(p += 5); await QtVsToolsPackage.Instance.JoinableTaskFactory.SwitchToMainThreadAsync(); UpdateInitStatus(p += 5); VcProject = Project.Object as VCProject; if (VcProject == null) return; UpdateInitStatus(p += 10); var context = Project.Object as IVsBrowseObjectContext; if (context == null) return; UpdateInitStatus(p += 10); UnconfiguredProject = context.UnconfiguredProject; if (UnconfiguredProject == null || UnconfiguredProject.ProjectService == null || UnconfiguredProject.ProjectService.Services == null) return; await TaskScheduler.Default; UpdateInitStatus(p += 10); var configs = await UnconfiguredProject.Services .ProjectConfigurationsService.GetKnownProjectConfigurationsAsync(); UpdateInitStatus(p += 10); Initialized.Set(); Subscribers = new List(); int n = configs.Count; int d = (100 - p) / (n * 2); foreach (var config in configs) { var configProject = await UnconfiguredProject.LoadConfiguredProjectAsync(config); UpdateInitStatus(p += d); Subscribers.Add(new Subscriber(this, configProject)); configProject.ProjectUnloading += OnProjectUnloadingAsync; if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) { Messages.Print(string.Format( "{0:HH:mm:ss.FFF} QtProjectTracker({1}): Started tracking [{2}] {3}", DateTime.Now, Thread.CurrentThread.ManagedThreadId, config.Name, UnconfiguredProject.FullPath)); } UpdateInitStatus(p += d); } } async Task OnProjectUpdateAsync(ConfiguredProject config, IProjectSubscriptionUpdate update) { var changes = update.ProjectChanges.Values .Where(x => x.Difference.AnyChanges) .Select(x => x.Difference); var changesCount = changes .Select(x => x.AddedItems.Count + x.ChangedItems.Count + x.ChangedProperties.Count + x.RemovedItems.Count + x.RenamedItems.Count) .Sum(); var changedProps = changes.SelectMany(x => x.ChangedProperties); if (changesCount == 0 || (changesCount == 1 && changedProps.Count() == 1 && changedProps.First() == "QtLastBackgroundBuild")) { return; } if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) { Messages.Print(string.Format( "{0:HH:mm:ss.FFF} QtProjectTracker({1}): Changed [{2}] {3}", DateTime.Now, Thread.CurrentThread.ManagedThreadId, config.ProjectConfiguration.Name, config.UnconfiguredProject.FullPath)); } await QtProjectIntellisense.RefreshAsync(Project, config.ProjectConfiguration.Name); } async Task OnProjectUnloadingAsync(object sender, EventArgs args) { var project = sender as ConfiguredProject; if (project == null || project.Services == null) return; if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) { Messages.Print(string.Format( "{0:HH:mm:ss.FFF} QtProjectTracker: Stopped tracking [{1}] {2}", DateTime.Now, project.ProjectConfiguration.Name, project.UnconfiguredProject.FullPath)); } lock (CriticalSection) { if (Subscribers != null) { Subscribers.ForEach(s => s.Dispose()); Subscribers.Clear(); Subscribers = null; } project.ProjectUnloading -= OnProjectUnloadingAsync; Instances.TryRemove(project.UnconfiguredProject.FullPath, out QtProjectTracker _); } await Task.Yield(); } void BeginInitStatus() { ThreadHelper.ThrowIfNotOnUIThread(); lock (StaticCriticalSection) { if (InitStatus != null) return; try { InitStatus = StatusCenter.PreRegister( new TaskHandlerOptions { Title = "Qt VS Tools: Setting up project tracking..." }, new TaskProgressData { ProgressText = string.Format("{0} ({1} projects remaining)", Project.Name, InitQueue.Count), CanBeCanceled = true, PercentComplete = 0 }) as ITaskHandler2; } catch (Exception e) { Messages.Print( e.Message + "\r\n\r\nStacktrace:\r\n" + e.StackTrace); } InitStatus.RegisterTask(new Task(() => throw new InvalidOperationException())); } } void UpdateInitStatus(int percentComplete) { ThreadHelper.ThrowIfNotOnUIThread(); lock (StaticCriticalSection) { if (InitStatus == null) return; try { InitStatus.Progress.Report(new TaskProgressData { ProgressText = string.Format("{0} ({1} project(s) remaining)", Project.Name, InitQueue.Count), CanBeCanceled = true, PercentComplete = percentComplete }); } catch (Exception e) { Messages.Print( e.Message + "\r\n\r\nStacktrace:\r\n" + e.StackTrace); } } } void EndInitStatus() { lock (StaticCriticalSection) { if (InitStatus == null) return; InitStatus.Dismiss(); InitStatus = null; } } } }