1235 lines
39 KiB
C#
1235 lines
39 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using EnvDTE;
|
|
using Microsoft.VisualStudio;
|
|
using Microsoft.VisualStudio.Shell;
|
|
using Microsoft.VisualStudio.Shell.Interop;
|
|
using Microsoft.VisualStudio.Text.Differencing;
|
|
using Microsoft.VisualStudio.Threading;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.Design;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection.Metadata;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Process = System.Diagnostics.Process;
|
|
|
|
namespace UnrealVS
|
|
{
|
|
using Task = System.Threading.Tasks.Task;
|
|
|
|
internal class P4Commands : IDisposable
|
|
{
|
|
private const bool bPullWorkingDirectoryOn = true;
|
|
private const bool bPullWorkingDirectoryOff = false;
|
|
|
|
private const bool bOutputStdOutOn = true;
|
|
private const bool bOutputStdOutOff = false;
|
|
|
|
private const int P4SubMenuID = 0x3100;
|
|
private const int P4CheckoutButtonID = 0x1450;
|
|
private const int P4AnnotateButtonID = 0x1451;
|
|
private const int P4ViewSelectedCLButtonID = 0x1452;
|
|
private const int P4IntegrationAwareTimelapseButtonID = 0x1453;
|
|
private const int P4DiffinVSButtonID = 0x1454;
|
|
private const int P4GetLast10ChangesID = 0x1455;
|
|
private const int P4ShowFileinP4VID = 0x1456;
|
|
private const int P4FastReconcileCodeFilesID = 0x1457;
|
|
private const int P4TimelapseButtonID = 0x1458;
|
|
private const int P4RevisionGraphButtonID = 0x1459;
|
|
private const int P4FileHistoryButtonID = 0x1460;
|
|
private const int P4RevertButtonID = 0x1461;
|
|
|
|
private OleMenuCommand SubMenuCommand;
|
|
|
|
//private System.Diagnostics.Process ChildProcess;
|
|
|
|
private List<Process> ChildProcessList = new List<Process>();
|
|
|
|
private IVsOutputWindowPane P4OutputPane;
|
|
private string P4WorkingDirectory;
|
|
private bool bPullWorkingDirectorFromP4 = true;
|
|
private static object bPullWorkingDirectorFromP4Lock = new object();
|
|
|
|
// Exe paths
|
|
private string P4Exe = "C:\\Program Files\\Perforce\\p4.exe";
|
|
private string P4VCCmd = "C:\\Program Files\\Perforce\\p4vc.exe";
|
|
private string P4VCCmdBat = "C:\\Program Files\\Perforce\\p4vc.bat";
|
|
private string P4VExe = "C:\\Program Files\\Perforce\\p4v.exe";
|
|
|
|
// user info
|
|
private string Username = "";
|
|
private string Port = "";
|
|
private string Client = "";
|
|
private string Stream = "";
|
|
private string UserInfoComplete = "";
|
|
private readonly object CheckoutQueueLock = new object();
|
|
private readonly HashSet<string> CheckoutQueueFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private JoinableTask CheckoutTask;
|
|
|
|
private class P4Command
|
|
{
|
|
public readonly OleMenuCommand ButtonCommand;
|
|
|
|
public P4Command(int ButtonID, EventHandler ButtonHandler, Func<EnvDTE.Document, bool> QueryStatusHandler = null)
|
|
{
|
|
var cmd = new OleMenuCommand(new EventHandler(ButtonHandler), new CommandID(GuidList.UnrealVSCmdSet, ButtonID));
|
|
cmd.BeforeQueryStatus += (s, e) =>
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
((MenuCommand)s).Enabled = DTE.ActiveDocument != null && (QueryStatusHandler == null || QueryStatusHandler(DTE.ActiveDocument));
|
|
};
|
|
|
|
ButtonCommand = cmd;
|
|
UnrealVSPackage.Instance.MenuCommandService.AddCommand(ButtonCommand);
|
|
}
|
|
|
|
public void Toggle(bool Enabled)
|
|
{
|
|
ButtonCommand.Visible = ButtonCommand.Enabled = Enabled;
|
|
}
|
|
}
|
|
|
|
private readonly List<P4Command> P4CommandsList = new List<P4Command>(16);
|
|
|
|
private InterceptSave Interceptor;
|
|
|
|
private bool IsSolutionLoaded()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
return DTE.Solution.FileName.Length > 0;
|
|
}
|
|
|
|
private string GetCheckoutQueueFileName()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
if (DTE.Solution.FileName.Length > 0)
|
|
{
|
|
return Path.Combine(Path.GetDirectoryName(DTE.Solution.FileName), ".p4checkout.txt");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void OnQuickBuildSubMenuQuery(object sender, EventArgs e)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
bool bEnableCommands = UnrealVSPackage.Instance.OptionsPage.AllowUnrealVSP4;
|
|
bool bSolutionLoaded = IsSolutionLoaded();
|
|
|
|
var SenderSubMenuCommand = (OleMenuCommand)sender;
|
|
|
|
SenderSubMenuCommand.Visible = SenderSubMenuCommand.Enabled = bEnableCommands & bSolutionLoaded;
|
|
}
|
|
|
|
public P4Commands()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
// setup callbacks on IDE operations
|
|
UnrealVSPackage.Instance.OptionsPage.OnOptionsChanged += OnOptionsChanged;
|
|
UnrealVSPackage.Instance.OnSolutionOpened += SolutionOpened;
|
|
UnrealVSPackage.Instance.OnSolutionClosed += SolutionClosed;
|
|
|
|
// create specific output window for unrealvs.P4
|
|
P4OutputPane = UnrealVSPackage.Instance.GetP4OutputPane();
|
|
|
|
// figure out the P4VC path
|
|
if (!File.Exists(P4VCCmd))
|
|
{
|
|
if (File.Exists(P4VCCmdBat))
|
|
{
|
|
P4VCCmd = P4VCCmdBat;
|
|
}
|
|
else
|
|
{
|
|
P4OutputPane.Activate();
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4VC not found, {P4VCCmd} or {P4VCCmdBat}{Environment.NewLine}");
|
|
P4VCCmd = "";
|
|
}
|
|
|
|
}
|
|
|
|
// add commands
|
|
P4CommandsList.Add(new P4Command(P4CheckoutButtonID, P4CheckoutButtonHandler));
|
|
P4CommandsList.Add(new P4Command(P4RevertButtonID, P4RevertButtonHandler));
|
|
P4CommandsList.Add(new P4Command(P4AnnotateButtonID, P4AnnotateButtonHandler));
|
|
P4CommandsList.Add(new P4Command(P4DiffinVSButtonID, P4DiffHandler));
|
|
P4CommandsList.Add(new P4Command(P4GetLast10ChangesID, P4GetLast10ChangesHandler));
|
|
P4CommandsList.Add(new P4Command(P4FastReconcileCodeFilesID, P4FastReconcileCodeFiles));
|
|
|
|
if (P4VCCmd.Length > 1)
|
|
{
|
|
P4CommandsList.Add(new P4Command(P4IntegrationAwareTimelapseButtonID, P4IntegrationAwareTimeLapseHandler));
|
|
P4CommandsList.Add(new P4Command(P4ShowFileinP4VID, P4ShowFileInP4VHandler));
|
|
P4CommandsList.Add(new P4Command(P4FileHistoryButtonID, P4FileHistoryHandler));
|
|
P4CommandsList.Add(new P4Command(P4RevisionGraphButtonID, P4RevisionGraphHandler));
|
|
P4CommandsList.Add(new P4Command(P4TimelapseButtonID, P4TimelapseHandler));
|
|
P4CommandsList.Add(new P4Command(P4ViewSelectedCLButtonID, P4ViewSelectedCLButtonHandler, (Document) =>
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
TextSelection TextSel = (TextSelection)Document.Selection;
|
|
return !string.IsNullOrEmpty(TextSel.Text) && int.TryParse(TextSel.Text, out _);
|
|
}));
|
|
}
|
|
|
|
// add sub menu for commands
|
|
SubMenuCommand = new OleMenuCommand(null, new CommandID(GuidList.UnrealVSCmdSet, P4SubMenuID));
|
|
SubMenuCommand.BeforeQueryStatus += OnQuickBuildSubMenuQuery;
|
|
UnrealVSPackage.Instance.MenuCommandService.AddCommand(SubMenuCommand);
|
|
|
|
// Update the menu visibility to enforce user options.
|
|
UpdateMenuOptions();
|
|
|
|
var runningDocumentTable = new RunningDocumentTable(UnrealVSPackage.Instance);
|
|
Interceptor = new InterceptSave(UnrealVSPackage.Instance.DTE, runningDocumentTable, this);
|
|
|
|
runningDocumentTable.Advise(Interceptor);
|
|
|
|
string CheckoutQueueFile = GetCheckoutQueueFileName();
|
|
if (CheckoutQueueFile != null)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe("UnrealVS started" + Environment.NewLine);
|
|
if (File.Exists(P4Exe))
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"Using checkout queue: {CheckoutQueueFile}" + Environment.NewLine);
|
|
PulseCheckoutQueue(CheckoutQueueFile);
|
|
}
|
|
else
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"Unable to find Perforce executable ({P4Exe}). Perforce checkout queue will be disabled." + Environment.NewLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when solutions are loaded or unloaded
|
|
private void SolutionOpened()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
// Update the menu visibility
|
|
UpdateMenuOptions();
|
|
|
|
string CheckoutQueueFile = GetCheckoutQueueFileName();
|
|
if (CheckoutQueueFile != null)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"Solution opened. Using checkout queue: {CheckoutQueueFile}" + Environment.NewLine);
|
|
PulseCheckoutQueue(CheckoutQueueFile);
|
|
}
|
|
}
|
|
|
|
private void SolutionClosed()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
// Update the menu visibility
|
|
UpdateMenuOptions();
|
|
|
|
// Clear any existing P4 working directory settings
|
|
P4WorkingDirectory = "";
|
|
bPullWorkingDirectorFromP4 = true;
|
|
}
|
|
|
|
private void UpdateMenuOptions()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
bool bEnableCommands = UnrealVSPackage.Instance.OptionsPage.AllowUnrealVSP4;
|
|
bool bSolutionLoaded = IsSolutionLoaded();
|
|
|
|
// update each menu item enabled command
|
|
foreach (P4Command command in P4CommandsList)
|
|
{
|
|
command.Toggle(bSolutionLoaded && bEnableCommands);
|
|
}
|
|
|
|
}
|
|
private void OnOptionsChanged(object Sender, EventArgs E)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
UpdateMenuOptions();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
KillChildProcess();
|
|
}
|
|
|
|
private void KillChildProcess()
|
|
{
|
|
foreach (var OldProcess in ChildProcessList)
|
|
{
|
|
if (!OldProcess.HasExited)
|
|
{
|
|
OldProcess.Kill();
|
|
OldProcess.WaitForExit();
|
|
}
|
|
OldProcess.Dispose();
|
|
}
|
|
}
|
|
|
|
private void P4RevertButtonHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
// Check we've got a file open
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4Checkout called without an active document");
|
|
|
|
P4OutputPane.Activate();
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4Revert called without an active document{Environment.NewLine}");
|
|
|
|
return;
|
|
}
|
|
|
|
if (VSConstants.MessageBoxResult.IDOK == (VSConstants.MessageBoxResult) VsShellUtilities.ShowMessageBox(UnrealVSPackage.Instance, string.Empty, $"Are you sure you want to revert {DTE.ActiveDocument.Name}?", OLEMSGICON.OLEMSGICON_QUERY, OLEMSGBUTTON.OLEMSGBUTTON_OKCANCEL, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST))
|
|
{
|
|
TryP4Command($"revert {DTE.ActiveDocument.FullName}", out _, out _);
|
|
}
|
|
}
|
|
|
|
private void P4CheckoutButtonHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
// Check we've got a file open
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4Checkout called without an active document");
|
|
|
|
P4OutputPane.Activate();
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4Checkout called without an active document{Environment.NewLine}");
|
|
|
|
return;
|
|
}
|
|
|
|
OpenForEdit(DTE.ActiveDocument.FullName);
|
|
}
|
|
private void P4AnnotateButtonHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
// Debug.ListCallStack
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
P4OutputPane.Activate();
|
|
|
|
// Check we've got a file open
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4Annotate called without an active document");
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4Annotate called without an active document{Environment.NewLine}");
|
|
|
|
return;
|
|
}
|
|
|
|
string CaptureP4Output;
|
|
string CaptureP4StdErr;
|
|
|
|
// Call annotate itself
|
|
bool bResult = TryP4Command($"annotate -TcIqu \"{DTE.ActiveDocument.FullName}", out CaptureP4Output, out CaptureP4StdErr, bPullWorkingDirectoryOn, bOutputStdOutOff);
|
|
|
|
if (!bResult || CaptureP4StdErr.Length > 0)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4Annotate call failed");
|
|
return;
|
|
}
|
|
|
|
// Extract the current document line number and the first line of text within it
|
|
string FirstLine;
|
|
int CurrentLine = 0;
|
|
{
|
|
TextDocument CurrentDocument = (TextDocument)(DTE.ActiveDocument.Object("TextDocument"));
|
|
|
|
var EditPoint = CurrentDocument.StartPoint.CreateEditPoint();
|
|
string DocumentContents = EditPoint.GetText(CurrentDocument.EndPoint);
|
|
FirstLine = DocumentContents.Split('\n')[0];
|
|
|
|
|
|
TextSelection TextSel = DTE.ActiveWindow.Selection as TextSelection;
|
|
if (TextSel != null)
|
|
{
|
|
CurrentLine = TextSel.CurrentLine;
|
|
}
|
|
}
|
|
|
|
// Pre-process the output to comment out the additions thus allowing
|
|
// code to use correct syntax coloring - helps enormously with visualization
|
|
StringBuilder EditedCopy = new StringBuilder();
|
|
{
|
|
// replace
|
|
// 13149436: First.Last 2020/05/04
|
|
//
|
|
// with
|
|
// /* 13149436: First.Last 2020/05/04*/
|
|
|
|
// This is the per line offset of the annotation added to the document
|
|
int AnnotateOffset = CaptureP4Output.IndexOf(FirstLine);
|
|
|
|
string[] AnnotateLines = CaptureP4Output.Split('\n');
|
|
|
|
foreach (string Line in AnnotateLines)
|
|
{
|
|
if (Line.Length > AnnotateOffset)
|
|
{
|
|
string EditedLine = Line.Insert(AnnotateOffset, "*/");
|
|
EditedLine = EditedLine.Insert(0, "/*");
|
|
|
|
EditedCopy.Append(EditedLine);
|
|
}
|
|
else
|
|
{
|
|
EditedCopy.Append(Line);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Replace GetTempPath with the UBT intermediate folder if we have access
|
|
string TempPath = Path.GetTempPath();
|
|
string TempFileName = Path.GetFileNameWithoutExtension(DTE.ActiveDocument.FullName) + "_annotate" + Path.GetExtension(DTE.ActiveDocument.FullName);
|
|
string TempFilePath = Path.Combine(TempPath, TempFileName);
|
|
|
|
// Write out our temp file
|
|
File.WriteAllText(TempFilePath, EditedCopy.ToString());
|
|
|
|
// Open it, activate it and move to the line the user focused to execute the command
|
|
DTE.ExecuteCommand("File.OpenFile", $"\"{TempFilePath}\"");
|
|
DTE.ActiveDocument.Activate();
|
|
|
|
TextSelection NewTextSel = DTE.ActiveWindow.Selection as TextSelection;
|
|
if (NewTextSel != null)
|
|
{
|
|
NewTextSel.GotoLine(CurrentLine, false);
|
|
}
|
|
}
|
|
private void P4ViewSelectedCLButtonHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TextSelection TextSel = (TextSelection)DTE.ActiveDocument.Selection;
|
|
|
|
if(Int32.TryParse(TextSel.Text, out int ChangeList) && ChangeList > 0)
|
|
{
|
|
TryP4VCCommand($"Change {ChangeList}");
|
|
}
|
|
}
|
|
|
|
private void P4TimelapseHandler(object sender, EventArgs args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4Timelapse called without an active document");
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4Timelapse called without an active document{Environment.NewLine}");
|
|
return;
|
|
}
|
|
|
|
string Command = "timelapse ";
|
|
|
|
TextDocument CurrentDocument = (TextDocument) DTE.ActiveDocument.Object("TextDocument");
|
|
if (CurrentDocument != null)
|
|
{
|
|
TextSelection TextSel = (TextSelection) DTE.ActiveDocument.Selection;
|
|
if (TextSel != null)
|
|
{
|
|
Command += $"-l {TextSel.CurrentLine} ";
|
|
}
|
|
}
|
|
|
|
Command += $"\"{DTE.ActiveDocument.FullName}\"";
|
|
|
|
TryP4VCCommand(Command, true);
|
|
}
|
|
|
|
private void P4RevisionGraphHandler(object sender, EventArgs args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4Timelapse called without an active document");
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4RevisionGraph called without an active document{Environment.NewLine}");
|
|
return;
|
|
}
|
|
|
|
string Command = $"revgraph \"{DTE.ActiveDocument.FullName}\"";
|
|
TryP4VCCommand(Command, true);
|
|
}
|
|
|
|
private void P4FileHistoryHandler(object sender, EventArgs args)
|
|
{
|
|
if (P4VCCmd.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4FileHistory called without an active document");
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4FileHistory called without an active document{Environment.NewLine}");
|
|
return;
|
|
}
|
|
|
|
string Command = $"history \"{DTE.ActiveDocument.FullName}\"";
|
|
TryP4VCCommand(Command);
|
|
}
|
|
|
|
private void P4IntegrationAwareTimeLapseHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4IntegrationAwareTimeLapse called without an active document");
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4IntegrationAwareTimeLapse called without an active document{Environment.NewLine}");
|
|
|
|
return;
|
|
}
|
|
|
|
string Command = $"-win 0 {UserInfoComplete} -cmd \"annotate -i \"{DTE.ActiveDocument.FullName}\"\"";
|
|
|
|
TryP4VCommand(Command);
|
|
}
|
|
|
|
private void ChangeDiffSetting()
|
|
{
|
|
if (UnrealVSPackage.Instance.OptionsPage.AllowUnrealVSOverrideDiffSettings)
|
|
{
|
|
bool bMargin = true;
|
|
UnrealVSPackage.Instance.EditorOptionsFactory.GlobalOptions.SetOptionValue("Diff/View/ShowDiffOverviewMargin", bMargin);
|
|
|
|
DifferenceHighlightMode HighlightMode = (DifferenceHighlightMode)3;
|
|
UnrealVSPackage.Instance.EditorOptionsFactory.GlobalOptions.SetOptionValue("Diff/View/HighlightMode", HighlightMode);
|
|
}
|
|
}
|
|
|
|
private void P4DiffHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
Logging.WriteLine("P4DiffInVSHandler called without an active document");
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"1>------ P4DiffInVSHandler called without an active document{Environment.NewLine}");
|
|
|
|
return;
|
|
}
|
|
|
|
if(UnrealVSPackage.Instance.OptionsPage.UseP4VDiff && P4VCCmd.Length > 1)
|
|
{
|
|
TryP4VCCommand($"diffhave \"{DTE.ActiveDocument.FullName}\"");
|
|
return;
|
|
}
|
|
|
|
ChangeDiffSetting();
|
|
|
|
string CaptureP4Output = "";
|
|
|
|
// get the HAVE revision
|
|
// p4 fstat -T "haveRev" -Olp //UE5/Main/Engine/Source/Programs/UnrealVS/UnrealVS.Shared/P4Commands.cs
|
|
if (TryP4Command($"fstat -T \"haveRev,depotFile\" -Olp \"{DTE.ActiveDocument.FullName}\"", out CaptureP4Output, out _))
|
|
{
|
|
// expect output of the form
|
|
// "... haveRev 5"
|
|
// "... depotFile //UE5/Main/Engine/Source/Programs/UnrealVS/UnrealVS.Shared/P4Commands.cs
|
|
Regex HavePattern = new Regex(@"... haveRev (?<Have>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
Regex DepotPathPattern = new Regex(@"... depotFile (?<depotFile>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
System.Text.RegularExpressions.Match HaveMatch = HavePattern.Match(CaptureP4Output);
|
|
System.Text.RegularExpressions.Match PathMatch = DepotPathPattern.Match(CaptureP4Output);
|
|
int HaveRev = Int32.Parse(HaveMatch.Groups["Have"].Value.Trim());
|
|
string depotPath = PathMatch.Groups["depotFile"].Value.Trim();
|
|
|
|
// Generate the Temp filename
|
|
string TempPath = Path.GetTempPath();
|
|
string TempFileName = Path.GetFileNameWithoutExtension(DTE.ActiveDocument.FullName) + "$" + HaveRev.ToString() + Path.GetExtension(DTE.ActiveDocument.FullName);
|
|
string TempFilePath = Path.Combine(TempPath, TempFileName);
|
|
|
|
// sync the HAVE revision to a file
|
|
// p4 print //UE5/Main/Engine/Source/Programs/UnrealVS/UnrealVS.Shared/P4Commands.cs#5 >> file
|
|
|
|
string CaptureP4Output2 = "";
|
|
|
|
string VersionPath = $"{depotPath}#{HaveRev}";
|
|
if (TryP4Command($"-q print \"{VersionPath}\"", out CaptureP4Output2, out _, bPullWorkingDirectoryOn, bOutputStdOutOff))
|
|
{
|
|
File.WriteAllText(TempFilePath, CaptureP4Output2);
|
|
|
|
// Tools.DiffFiles SourceFile, TargetFile, [SourceDisplayName],[TargetDisplayName]
|
|
DTE.ExecuteCommand("Tools.DiffFiles", $"\"{TempFilePath}\" \"{DTE.ActiveDocument.FullName}\" \"{VersionPath}\" \"{DTE.ActiveDocument.FullName}\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void P4GetLast10ChangesHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
string TempPath = Path.GetTempPath();
|
|
|
|
// generate the changes list
|
|
string ChangesTempFileName = Path.GetFileNameWithoutExtension(DTE.ActiveDocument.FullName) + "_last_10_changelists" + Path.GetExtension(DTE.ActiveDocument.FullName);
|
|
string ChangesTempFilePath = Path.Combine(TempPath, ChangesTempFileName);
|
|
|
|
string CaptureP4Output = "";
|
|
if (TryP4Command($"changes -itls submitted -m 10 \"{DTE.ActiveDocument.FullName}\"", out CaptureP4Output, out _, bOutputStdOutOff))
|
|
{
|
|
File.WriteAllText(ChangesTempFilePath, CaptureP4Output);
|
|
|
|
DTE.ExecuteCommand("File.OpenFile", $"\"{ChangesTempFilePath}\"");
|
|
DTE.ActiveDocument.Activate();
|
|
DTE.ExecuteCommand("Window.NewVerticalTabGroup");
|
|
}
|
|
}
|
|
|
|
private void P4FastReconcileCodeFiles(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
string[] DefaultExtensions =
|
|
{
|
|
"c*",
|
|
"h*",
|
|
"ini",
|
|
"uproject",
|
|
"uplugin"
|
|
};
|
|
|
|
string[] Extensions = UnrealVSPackage.Instance.OptionsPage.ReconcileExtensions.Split(';');
|
|
|
|
if (Extensions.Length < 1)
|
|
{
|
|
Extensions = DefaultExtensions;
|
|
}
|
|
|
|
// generate the changes list
|
|
string ReconcileDepotPaths = "";
|
|
string Preview = "-n";
|
|
|
|
if (string.IsNullOrWhiteSpace(P4WorkingDirectory))
|
|
{
|
|
PullWorkingDirectory(bPullWorkingDirectoryOn);
|
|
}
|
|
|
|
foreach (string Ext in Extensions)
|
|
{
|
|
ReconcileDepotPaths += P4WorkingDirectory + Path.DirectorySeparatorChar + "...." + Ext + " ";
|
|
}
|
|
|
|
if (UnrealVSPackage.Instance.OptionsPage.AllowReconcileToMarkForEdit)
|
|
{
|
|
Preview = "";
|
|
}
|
|
else
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"PREVIEW{Environment.NewLine}");
|
|
}
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"Running Async Reconcile on : {ReconcileDepotPaths}{Environment.NewLine}");
|
|
P4OutputPane.Activate();
|
|
|
|
_ = TryP4CommandAsync($"reconcile -e -m {Preview} {ReconcileDepotPaths}");
|
|
}
|
|
|
|
private void P4ShowFileInP4VHandler(object Sender, EventArgs Args)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
if (DTE.ActiveDocument == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(UserInfoComplete.Length == 0)
|
|
{
|
|
SetUserInfoStrings();
|
|
}
|
|
|
|
if (Username.Length > 0 && Port.Length > 0 && Client.Length > 0)
|
|
{
|
|
TryP4VCommand($"-p {Port} -u {Username} -c {Client} -s \"{DTE.ActiveDocument.FullName}\"");
|
|
}
|
|
else
|
|
{
|
|
TryP4VCommand($"-s \"{DTE.ActiveDocument.FullName}\"");
|
|
}
|
|
}
|
|
|
|
public void OpenForEdit(string FileName, bool CheckOptionsFlag = false)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
// if the callee requested it, honor the enabling of autocheckout option - return if its off
|
|
if (CheckOptionsFlag && !UnrealVSPackage.Instance.OptionsPage.AllowUnrealVSCheckoutOnEdit)
|
|
{
|
|
//P4OutputPane.OutputStringThreadSafe($"autocheckout disabled: {FileName}");
|
|
return;
|
|
}
|
|
|
|
// Don't open for edit if the file is already writable, unless the command was user-triggered
|
|
if (!File.Exists(FileName) ||
|
|
(CheckOptionsFlag && !File.GetAttributes(FileName).HasFlag(FileAttributes.ReadOnly)))
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"already writeable: {FileName}" + Environment.NewLine);
|
|
return;
|
|
}
|
|
|
|
if (UnrealVSPackage.Instance.OptionsPage.AllowAsyncP4Checkout)
|
|
{
|
|
string queueFile = GetCheckoutQueueFileName();
|
|
if (queueFile == null)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe("Unable to get path to checkout queue file. No solution loaded?" + Environment.NewLine);
|
|
return;
|
|
}
|
|
|
|
// Add the file to the queue for checking out before modifying it. This allows us to recover in the case of a P4 outage or VS exit.
|
|
Logging.WriteLine($"Adding async checkout of {FileName} to {queueFile}");
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
try
|
|
{
|
|
File.AppendAllLines(queueFile, new[] { FileName });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"Unable to update {queueFile} ({ex.Message})" + Environment.NewLine);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//P4OutputPane.OutputStringThreadSafe($"Marking file as writeable: {FileName}");
|
|
// Mark the file as writeable
|
|
FileAttributes NewAttributes = File.GetAttributes(FileName) & ~FileAttributes.ReadOnly;
|
|
File.SetAttributes(FileName, NewAttributes);
|
|
|
|
// Start a background thread to update the queue
|
|
PulseCheckoutQueue(queueFile);
|
|
}
|
|
else
|
|
{
|
|
string StdOut, StdErr;
|
|
|
|
// sync edit command - will lock the UI but be safer (no risk of a locally writeable file that is not checked out)
|
|
bool Success = TryP4Command($"edit \"{FileName}\"", out StdOut, out StdErr);
|
|
|
|
if (!Success && StdErr.Contains("file(s) not on client."))
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"{FileName} has not been added yet, adding now.{Environment.NewLine}");
|
|
TryP4Command($"add \"{FileName}\"", out _, out _);
|
|
}
|
|
}
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "TODO")]
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "VSSDK007:ThreadHelper.JoinableTaskFactory.RunAsync", Justification = "TODO")]
|
|
private void PulseCheckoutQueue(string QueueFile)
|
|
{
|
|
TaskCompletionSource<bool> StartTask = null;
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
CheckoutQueueFiles.Add(QueueFile);
|
|
|
|
if (CheckoutTask == null)
|
|
{
|
|
Logging.WriteLine($"Starting checkout task.");
|
|
StartTask = new TaskCompletionSource<bool>();
|
|
CheckoutTask = ThreadHelper.JoinableTaskFactory.RunAsync(async () => { await StartTask.Task; await UpdateCheckoutQueueAsync(); });
|
|
}
|
|
}
|
|
StartTask?.SetResult(true);
|
|
}
|
|
|
|
private async Task UpdateCheckoutQueueAsync()
|
|
{
|
|
for (; ; )
|
|
{
|
|
string QueueFile = null;
|
|
try
|
|
{
|
|
// Read the list of files that need checking out
|
|
string[] Lines;
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
while (QueueFile == null || !File.Exists(QueueFile))
|
|
{
|
|
QueueFile = CheckoutQueueFiles.FirstOrDefault();
|
|
if (QueueFile == null)
|
|
{
|
|
Logging.WriteLine($"Stopping checkout task; setting CheckoutTask to null.");
|
|
CheckoutTask = null;
|
|
return;
|
|
}
|
|
CheckoutQueueFiles.Remove(QueueFile);
|
|
}
|
|
Lines = File.ReadAllLines(QueueFile);
|
|
}
|
|
|
|
// Open each file for edit
|
|
bool Pause = false;
|
|
foreach (string Line in Lines)
|
|
{
|
|
string FileName = Line.Trim();
|
|
if (FileName.Length > 0)
|
|
{
|
|
bool Success = false;
|
|
|
|
(bool EditSuccess, string StdOut, string StdErr) = await TryP4CommandExAsync(P4Exe, $"edit \"{FileName}\"");
|
|
if (EditSuccess)
|
|
{
|
|
Success = true;
|
|
|
|
Logging.WriteLine($"Performed async checkout of {FileName}.");
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
string[] NewLines = File.ReadAllLines(QueueFile);
|
|
File.WriteAllLines(QueueFile, NewLines.Where(x => !x.Equals(Line, StringComparison.Ordinal)));
|
|
}
|
|
}
|
|
|
|
if (!EditSuccess && StdErr.Contains("file(s) not on client."))
|
|
{
|
|
await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
|
|
{
|
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
P4OutputPane.OutputStringThreadSafe($"{FileName} has not been added yet, adding now.{Environment.NewLine}");
|
|
});
|
|
(bool AddSuccess, string _, string _) = await TryP4CommandExAsync(P4Exe, $"add \"{FileName}\"");
|
|
|
|
if (AddSuccess)
|
|
{
|
|
Success = true;
|
|
|
|
Logging.WriteLine($"Performed async add of {FileName}.");
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
string[] NewLines = File.ReadAllLines(QueueFile);
|
|
File.WriteAllLines(QueueFile, NewLines.Where(x => !x.Equals(Line, StringComparison.Ordinal)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Success)
|
|
{
|
|
// Re-add to checkout queue, to force another loop
|
|
Logging.WriteLine($"Unable to check out file {FileName}. Will retry.");
|
|
CheckoutQueueFiles.Add(QueueFile);
|
|
Pause = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we've looped over everything and should pause
|
|
if (Pause)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(10.0));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.WriteLine($"Unable to update checkout queue: {ex}");
|
|
|
|
if (QueueFile != null)
|
|
{
|
|
lock (CheckoutQueueLock)
|
|
{
|
|
CheckoutQueueFiles.Add(QueueFile);
|
|
}
|
|
}
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(10.0));
|
|
}
|
|
}
|
|
}
|
|
|
|
private string ReadWorkingDirectorFromP4Config()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
string WorkingDirectory = "";
|
|
//------P4 Operation started
|
|
//P4CLIENT = andrew.firth_private_frosty2(config 'd:\p4\frosty\p4config.txt')
|
|
//P4CONFIG = p4config.txt(set)(config 'd:\p4\frosty\p4config.txt')
|
|
//P4EDITOR = C:\windows\SysWOW64\notepad.exe(set)
|
|
//P4PORT = perforce:1666(config 'd:\p4\frosty\p4config.txt')
|
|
//P4USER = andrew.firth(config 'd:\p4\frosty\p4config.txt')
|
|
//P4_perforce:1666_CHARSET = none(set)
|
|
|
|
string CaptureP4Output = "";
|
|
TryP4Command("set", out CaptureP4Output, out _, bPullWorkingDirectoryOff);
|
|
|
|
bool bSuccess = false;
|
|
|
|
string[] lines = CaptureP4Output.Split('\n');
|
|
foreach (string Line in lines)
|
|
{
|
|
string TrimLine = Line.Trim();
|
|
|
|
// Match this in https://regex101.com/ to debug it. Basically it's words=anything(anything'path-to-config-file'anything).
|
|
System.Text.RegularExpressions.Match Match = Regex.Match(TrimLine, @"^\w+=.*\(.*'(.*)'.*\)$");
|
|
if (Match.Success)
|
|
{
|
|
WorkingDirectory = Path.GetDirectoryName(Match.Groups[1].Value);
|
|
if (Directory.Exists(WorkingDirectory))
|
|
{
|
|
bSuccess = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bSuccess)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"Attempt to pull the P4WorkingDirectory from 'p4 set' failed. Have you run 'RunUAT P4WriteConfig' in your root directory?{Environment.NewLine}");
|
|
}
|
|
|
|
return WorkingDirectory;
|
|
}
|
|
|
|
void SetUserInfoStrings()
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
string CaptureP4Output;
|
|
TryP4Command($"-s -L \"{P4WorkingDirectory}\" info", out CaptureP4Output, out _, bPullWorkingDirectoryOff);
|
|
|
|
Regex UserPattern = new Regex(@"User name: (?<user>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
Regex PortPattern = new Regex(@"Server address: (?<port>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
Regex ClientPattern = new Regex(@"Client name: (?<client>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
Regex ClientStream = new Regex(@"Client stream: (?<stream>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
Regex ServerEncryption = new Regex(@"Server encryption: (?<encryption>.*)$", RegexOptions.Compiled | RegexOptions.Multiline);
|
|
|
|
System.Text.RegularExpressions.Match UserMatch = UserPattern.Match(CaptureP4Output);
|
|
System.Text.RegularExpressions.Match PortMatch = PortPattern.Match(CaptureP4Output);
|
|
System.Text.RegularExpressions.Match ClientMatch = ClientPattern.Match(CaptureP4Output);
|
|
System.Text.RegularExpressions.Match StreamMatch = ClientStream.Match(CaptureP4Output);
|
|
System.Text.RegularExpressions.Match EncryptionMatch = ServerEncryption.Match(CaptureP4Output);
|
|
|
|
Port = PortMatch.Groups["port"].Value.Trim();
|
|
Username = UserMatch.Groups["user"].Value.Trim();
|
|
Client = ClientMatch.Groups["client"].Value.Trim();
|
|
Stream = StreamMatch.Groups["stream"].Value.Trim();
|
|
|
|
if (EncryptionMatch.Groups["encryption"].Value.Trim().Equals("encrypted", StringComparison.OrdinalIgnoreCase) && !Port.StartsWith("ssl:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Port = "ssl:" + Port;
|
|
}
|
|
|
|
UserInfoComplete = string.Format(" -p {0} -u {1} -c {2} ", Port, Username, Client);
|
|
|
|
P4OutputPane.OutputStringThreadSafe("GetUserInfoStringFull : " + UserInfoComplete + Environment.NewLine);
|
|
}
|
|
|
|
private void PullWorkingDirectory(bool bPullfromP4Settings)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
lock(bPullWorkingDirectorFromP4Lock)
|
|
{
|
|
if (IsSolutionLoaded() && (P4WorkingDirectory == null || P4WorkingDirectory.Length < 2))
|
|
{
|
|
DTE DTE = UnrealVSPackage.Instance.DTE;
|
|
|
|
// use the current solution folder as a temp working directory
|
|
P4WorkingDirectory = Path.GetDirectoryName(DTE.Solution.FileName);
|
|
|
|
// SetUserInfoStrings
|
|
SetUserInfoStrings();
|
|
}
|
|
|
|
// if the callee wants us to pull a CWD and it wasn't already done
|
|
// pull it from the p4 config now
|
|
if (bPullfromP4Settings && bPullWorkingDirectorFromP4)
|
|
{
|
|
string NewWorkingDirectory = ReadWorkingDirectorFromP4Config();
|
|
|
|
if (NewWorkingDirectory.Length > 1)
|
|
{
|
|
P4WorkingDirectory = NewWorkingDirectory;
|
|
bPullWorkingDirectorFromP4 = false;
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"P4WorkingDirectory set to '{P4WorkingDirectory}'{Environment.NewLine}");
|
|
}
|
|
else
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe($"P4WorkingDirectory set failed {Environment.NewLine}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<bool> TryP4CommandAsync(string CommandLine, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
(bool Result, _, _) = await TryP4CommandExAsync(P4Exe, CommandLine, bPullWorkingDirectoryNow, bOutputStdOut);
|
|
return Result;
|
|
}
|
|
|
|
private bool TryP4Command(string CommandLine, out string CaptureP4Output, out string CaptureP4StdErr, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
return TryP4CommandEx(P4Exe, CommandLine, out CaptureP4Output, out CaptureP4StdErr, bPullWorkingDirectoryNow, bOutputStdOut);
|
|
}
|
|
|
|
private bool TryP4VCCommand(string CommandLine, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
if (P4VCCmd.Length > 1)
|
|
{
|
|
return TryP4CommandEx(P4VCCmd, CommandLine, out _, out _, bPullWorkingDirectoryNow, bOutputStdOut);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void TryP4VCommand(string CommandLine, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
_ = TryP4CommandExAsync(P4VExe, CommandLine, bPullWorkingDirectoryNow, bOutputStdOut);
|
|
}
|
|
|
|
private bool TryP4CommandEx(string CmdPath, string CommandLine, out string CaptureP4StdOut, out string CaptureP4StdErr, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
(bool Result, string StdOut, string StdErr) = ThreadHelper.JoinableTaskFactory.Run(() => TryP4CommandExAsync(CmdPath, CommandLine, bPullWorkingDirectoryNow, bOutputStdOut));
|
|
|
|
CaptureP4StdOut = StdOut;
|
|
CaptureP4StdErr = StdErr;
|
|
|
|
return Result;
|
|
}
|
|
|
|
private async Task<(bool Result, string StdOut, string StdErr)> TryP4CommandExAsync(string CmdPath, string CommandLine, bool bPullWorkingDirectoryNow = bPullWorkingDirectoryOn, bool bOutputStdOut = bOutputStdOutOn)
|
|
{
|
|
await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
|
|
{
|
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
PullWorkingDirectory(bPullWorkingDirectoryNow);
|
|
|
|
// Set up the output pane
|
|
if (UnrealVSPackage.Instance.OptionsPage.ForceOutputWindow)
|
|
{
|
|
P4OutputPane.Activate();
|
|
}
|
|
|
|
P4OutputPane.OutputStringThreadSafe($"Running \"{CmdPath}\" {CommandLine}{Environment.NewLine}");
|
|
});
|
|
|
|
const int PollIntervalMs = 100;
|
|
const int TimeOutS = 10;
|
|
|
|
bool bTimedOut = false;
|
|
|
|
StringBuilder StdOutSB = new StringBuilder();
|
|
StringBuilder StdErrSB = new StringBuilder();
|
|
DateTime Start = DateTime.Now;
|
|
|
|
// Spawn the new process
|
|
System.Diagnostics.Process ChildProcess = new System.Diagnostics.Process()
|
|
{
|
|
StartInfo = new ProcessStartInfo()
|
|
{
|
|
FileName = CmdPath,
|
|
Arguments = CommandLine,
|
|
WorkingDirectory = P4WorkingDirectory,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
// Create a delegate for handling output messages
|
|
ChildProcess.OutputDataReceived += (s, a) => { if (a.Data != null) StdOutSB.AppendLine(a.Data); };
|
|
ChildProcess.ErrorDataReceived += (s, a) => { if (a.Data != null) StdErrSB.AppendLine(a.Data); };
|
|
ChildProcess.EnableRaisingEvents = true;
|
|
|
|
TaskCompletionSource<bool> ProcessExitTaskSource = new TaskCompletionSource<bool>();
|
|
ChildProcess.Exited += (s, a) =>
|
|
{
|
|
ProcessExitTaskSource.TrySetResult(true);
|
|
};
|
|
|
|
lock (ChildProcessList)
|
|
{
|
|
ChildProcessList.Add(ChildProcess);
|
|
}
|
|
|
|
Stopwatch ProcessStartTime = new Stopwatch();
|
|
ProcessStartTime.Start();
|
|
|
|
try
|
|
{
|
|
ChildProcess.Start();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return (false, "", $"Unable to launch {CmdPath} ({ex.Message})");
|
|
}
|
|
|
|
ChildProcess.BeginOutputReadLine();
|
|
ChildProcess.BeginErrorReadLine();
|
|
|
|
await ProcessExitTaskSource.Task;
|
|
|
|
while (!ChildProcess.WaitForExit(PollIntervalMs))
|
|
{
|
|
if (ProcessStartTime.Elapsed.Seconds >= TimeOutS)
|
|
{
|
|
bTimedOut = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
lock (ChildProcessList)
|
|
{
|
|
ChildProcessList.Remove(ChildProcess);
|
|
}
|
|
|
|
if (!bTimedOut)
|
|
{
|
|
string StdOut = StdOutSB.ToString();
|
|
string StdErr = StdErrSB.ToString();
|
|
|
|
await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
|
|
{
|
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
if (StdOut.Length > 0 && bOutputStdOut)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe(StdOut);
|
|
}
|
|
if (StdErr.Length > 0)
|
|
{
|
|
P4OutputPane.OutputStringThreadSafe(StdErr);
|
|
}
|
|
|
|
TimeSpan Duration = DateTime.Now - Start;
|
|
P4OutputPane.OutputStringThreadSafe($"complete {Duration.TotalSeconds.ToString()} {Environment.NewLine}");
|
|
});
|
|
|
|
bool Result = (ChildProcess.ExitCode == 0 && StdErr.Length == 0);
|
|
return (Result, StdOut, StdErr);
|
|
}
|
|
else
|
|
{
|
|
return (false, "", $"Timed out while trying to run: {CmdPath} {CommandLine}");
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class InterceptSave : IVsRunningDocTableEvents3
|
|
{
|
|
private readonly DTE DTE;
|
|
private readonly RunningDocumentTable RunningDocumentTable;
|
|
private readonly P4Commands P4Ops;
|
|
|
|
public InterceptSave(DTE InDTE, RunningDocumentTable InRrunningDocumentTable, P4Commands InP4Ops)
|
|
{
|
|
DTE = InDTE;
|
|
RunningDocumentTable = InRrunningDocumentTable;
|
|
P4Ops = InP4Ops;
|
|
}
|
|
|
|
public int OnBeforeSave(uint DocumentCookie)
|
|
{
|
|
ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
|
RunningDocumentInfo DocumentInfo = RunningDocumentTable.GetDocumentInfo(DocumentCookie);
|
|
|
|
string AbsoluteFilePath = DocumentInfo.Moniker;
|
|
|
|
P4Ops.OpenForEdit(AbsoluteFilePath, true);
|
|
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
// we are only using OnBeforeSave - but the interface requires us to define the whole interface.
|
|
public int OnAfterFirstDocumentLock(uint docCookie, uint dwRdtLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRdtLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int OnAfterSave(uint docCookie)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int OnAfterAttributeChange(uint docCookie, uint grfAttribs)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
int IVsRunningDocTableEvents3.OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld,
|
|
string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
|
|
int IVsRunningDocTableEvents2.OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld,
|
|
string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew)
|
|
{
|
|
return VSConstants.S_OK;
|
|
}
|
|
}
|
|
}
|