diff --git a/source/P4VFS.Console/P4VFS.Notes.txt b/source/P4VFS.Console/P4VFS.Notes.txt index 89549a7..0b968ce 100644 --- a/source/P4VFS.Console/P4VFS.Notes.txt +++ b/source/P4VFS.Console/P4VFS.Notes.txt @@ -1,5 +1,20 @@ Microsoft P4VFS Release Notes +Version [1.29.3.0] +* Perforce SSO login from URL now uses impersonated p4vfs login command and ShellExecute + instead of using cmd.exe. This fixes rare case lingering cmd.exe processes from failed + launch of the browser when there's no desktop session. This also prevents cmd.exe + with inherited handles, including TCP ports, from remaining open after service + is stopped and possibly causing failed port reopening after reinstall. +* Added option 'login -u ' for handling login challenge in web browser +* Added option 'login -t ' for timeout of login command before self + termination. This offers safety from possible lingering interactive prompts. +* Fixing placeholder file detection to ignore NTFS Zone and DLP streams for + file disk size calculation. This is primarily used in verification tests. +* Addition of ShellLoginTimeoutTest with simulation of Perforce server extension + similar to Helix Authentication Service for testing response to sso-auth-check + challenge from URL + Version [1.29.2.0] * Replacing authentication for public driver codesign from Hardware Development Center from deprecated client secret to x509 certificate. This is similar to diff --git a/source/P4VFS.Console/Source/Program.cs b/source/P4VFS.Console/Source/Program.cs index 7b0d75a..d0e13e1 100644 --- a/source/P4VFS.Console/Source/Program.cs +++ b/source/P4VFS.Console/Source/Program.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using Microsoft.P4VFS.Extensions; using Microsoft.P4VFS.Extensions.Linq; using Microsoft.P4VFS.Extensions.Utilities; @@ -57,6 +58,8 @@ sync Synchronize the client with its view of the depot. info Print out client/server information set Modify current service settings temporarily for this login session. resident Modify current resident status of local files. + hydrate Change file status to resident state (full downloaded size). + dehydrate Change file status to virtual state (zero downloaded size). populate Perform sync as fast as possible using quiet, single flush reconfig Modify the perforce configuration of local placeholder files. monitor Launch and control the P4VFS monitor application. @@ -207,10 +210,12 @@ p4vfs uninstall [-s -d] {"login", @" login Login to the perforce server and update the current ticket. - p4vfs login [-i -w] [password] + p4vfs login [-i -w] [-u ] [-t ] [password] -i Display a modal dialog for password entry -w Write the password to stdout after entering + -u Open a browser window with login challenge URL + -t Timeout waiting for login to complete "}, {"test", @" @@ -993,6 +998,9 @@ private static bool CommandLogin(string[] args) { bool interactive = false; bool writePasswd = false; + int timeoutSeconds = 0; + string shellUrl = null; + int argIndex = 0; for (; argIndex < args.Length; ++argIndex) { @@ -1004,12 +1012,52 @@ private static bool CommandLogin(string[] args) { writePasswd = true; } + else if (String.Compare(args[argIndex], "-t") == 0 && argIndex+1 < args.Length) + { + if (Int32.TryParse(args[++argIndex], out timeoutSeconds) == false) + { + VirtualFileSystemLog.Error("Invalid login timeout specified: {0}", args[argIndex]); + return false; + } + } + else if (String.Compare(args[argIndex], "-u") == 0 && argIndex+1 < args.Length) + { + shellUrl = args[++argIndex]; + if (Uri.IsWellFormedUriString(shellUrl, UriKind.Absolute) == false) + { + VirtualFileSystemLog.Error("Invalid shell URL specified: {0}", shellUrl); + return false; + } + } else { break; } } + CancellationToken cancellationToken = CancellationToken.None; + if (timeoutSeconds > 0) + { + cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)).Token; + } + + if (String.IsNullOrEmpty(shellUrl) == false) + { + VirtualFileSystemLog.Info("Login from URL: {0}", shellUrl); + ProcessInfo.ExecuteResult executeResult = ProcessInfo.ExecuteWait(new ProcessInfo.ExecuteParams{ + FileName = shellUrl, + UseShell = true, + LogOutput = false, + LogCommand = false, + CancellationToken = cancellationToken + }); + if (executeResult.WasCanceled) + { + VirtualFileSystemLog.Info("Timeout waiting for shell login from URL"); + } + return true; + } + DepotConfig p4Config = DepotInfo.DepotConfigFromPath(_P4Directory); if (String.IsNullOrEmpty(_P4Port)) @@ -1064,13 +1112,15 @@ private static bool CommandLogin(string[] args) { if (interactive) { - var thread = new System.Threading.Thread(new System.Threading.ThreadStart(() => + Thread thread = new Thread(new ThreadStart(() => { - var dlg = new Microsoft.P4VFS.Extensions.Controls.LoginWindow(); + Extensions.Controls.LoginWindow dlg = new Extensions.Controls.LoginWindow(); dlg.Port = _P4Port; dlg.Client = _P4Client; dlg.User = _P4User; dlg.Passwd = _P4Passwd; + dlg.CancellationToken = cancellationToken; + if (dlg.ShowDialog() == true) { _P4Passwd = dlg.Passwd; @@ -1081,14 +1131,25 @@ private static bool CommandLogin(string[] args) } })); - thread.SetApartmentState(System.Threading.ApartmentState.STA); + thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); } else if (String.IsNullOrEmpty(_P4Passwd)) { - System.Console.Write("Enter Password: "); - _P4Passwd = ConsoleReader.ReadLine(); + try + { + System.Console.Write("Enter Password: "); + _P4Passwd = ConsoleReader.ReadLine(cancellationToken); + } + catch (OperationCanceledException) + {} + } + + if (cancellationToken.IsCancellationRequested) + { + VirtualFileSystemLog.Info("Timeout waiting for password to login"); + return false; } } @@ -1243,7 +1304,7 @@ private static bool PredicateLogRetry(Func expression, string message, int return true; } - System.Threading.Thread.Sleep(retryWait); + Thread.Sleep(retryWait); } while (DateTime.Now < endTime); diff --git a/source/P4VFS.Core/Include/FileOperations.h b/source/P4VFS.Core/Include/FileOperations.h index 44dfb54..9af9fa5 100644 --- a/source/P4VFS.Core/Include/FileOperations.h +++ b/source/P4VFS.Core/Include/FileOperations.h @@ -239,7 +239,7 @@ namespace FileOperations { CreateProcessImpersonated( const WCHAR* commandLine, const WCHAR* currentDirectory, - BOOL waitForExit, + FileCore::Process::ExecuteFlags::Enum flags, FileCore::String* stdOutput = nullptr, const FileCore::UserContext* context = nullptr ); diff --git a/source/P4VFS.Core/Source/DepotClient.cpp b/source/P4VFS.Core/Source/DepotClient.cpp index a0d8b4a..2e127fe 100644 --- a/source/P4VFS.Core/Source/DepotClient.cpp +++ b/source/P4VFS.Core/Source/DepotClient.cpp @@ -291,8 +291,9 @@ class DepotClientCommand : public ClientUser, IDepotClientCommand if (url != nullptr && FileCore::StringInfo::IsNullOrEmpty(url->Text()) == false) { UserContext* context = m_Client->GetUserContext(); - DepotString cmd = StringInfo::Format("cmd.exe /c start %s", url->Text()); - FileOperations::CreateProcessImpersonated(CSTR_ATOW(cmd), nullptr, FALSE, nullptr, context); + WString cmd = StringInfo::Format(L"\"%s\\p4vfs.exe\" %s login -t 60 -u \"%s\"", FileInfo::FolderPath(FileInfo::ApplicationFilePath().c_str()).c_str(), CSTR_ATOW(m_Client->Config().ToCommandString()), CSTR_ATOW(url->Text())); + Process::ExecuteFlags::Enum flags = Process::ExecuteFlags::HideWindow; + FileOperations::CreateProcessImpersonated(cmd.c_str(), nullptr, flags, nullptr, context); } } @@ -975,7 +976,8 @@ bool FDepotClient::RequestInteractivePassword(DepotString& passwd) WString output; UserContext* context = m_P4->m_FileContext ? m_P4->m_FileContext->m_UserContext : nullptr; WString cmd = StringInfo::Format(L"\"%s\\p4vfs.exe\" %s login -i -w", FileInfo::FolderPath(FileInfo::ApplicationFilePath().c_str()).c_str(), CSTR_ATOW(m_P4->m_Config.ToCommandString())); - if (SUCCEEDED(FileOperations::CreateProcessImpersonated(cmd.c_str(), nullptr, TRUE, &output, context))) + Process::ExecuteFlags::Enum flags = Process::ExecuteFlags::WaitForExit | Process::ExecuteFlags::HideWindow; + if (SUCCEEDED(FileOperations::CreateProcessImpersonated(cmd.c_str(), nullptr, flags, &output, context))) { WStringArray lines = StringInfo::Split(output.c_str(), L"\n\r", StringInfo::SplitFlags::RemoveEmptyEntries); for (const WString& line : lines) diff --git a/source/P4VFS.Core/Source/FileCore.cpp b/source/P4VFS.Core/Source/FileCore.cpp index a05b959..2b84d1e 100644 --- a/source/P4VFS.Core/Source/FileCore.cpp +++ b/source/P4VFS.Core/Source/FileCore.cpp @@ -1443,14 +1443,34 @@ int64_t FileInfo::FileDiskSize(const wchar_t* filePath) const STREAM_LAYOUT_ENTRY* streamEntry = reinterpret_cast(streamEntryOrigin); streamEntryOffset = streamEntry->NextStreamOffset; if (streamEntry->AttributeFlags & FILE_ATTRIBUTE_SPARSE_FILE) + { continue; + } - const WCHAR dataStreamName[] = L":$DATA"; - const size_t dataStreamNameSize = (_countof(dataStreamName)-1)*sizeof(WCHAR); if (streamEntry->StreamIdentifierLength == 0) + { fileSize.QuadPart += streamEntry->AllocationSize.QuadPart; - else if (streamEntry->StreamIdentifierLength >= dataStreamNameSize && memcmp(dataStreamName, reinterpret_cast(streamEntry->StreamIdentifier)+streamEntry->StreamIdentifierLength-dataStreamNameSize, dataStreamNameSize) == 0) + continue; + } + + auto isMatchingStreamEntryName = [streamEntry](const WCHAR* dataName) -> bool + { + const size_t dataNameLength = wcslen(dataName); + const size_t streamNameLength = streamEntry->StreamIdentifierLength / sizeof(WCHAR); + return (streamNameLength >= dataNameLength && _wcsnicmp(dataName, reinterpret_cast(streamEntry->StreamIdentifier) + streamNameLength - dataNameLength, dataNameLength) == 0); + }; + + if (isMatchingStreamEntryName(L":SEC.ENDPOINTDLP:$DATA") || + isMatchingStreamEntryName(L":ZONE.IDENTIFIER:$DATA")) + { + continue; + } + + if (isMatchingStreamEntryName(L":$DATA")) + { fileSize.QuadPart += streamEntry->AllocationSize.QuadPart; + continue; + } } break; } diff --git a/source/P4VFS.Core/Source/FileOperations.cpp b/source/P4VFS.Core/Source/FileOperations.cpp index 2d3f184..cf87fa8 100644 --- a/source/P4VFS.Core/Source/FileOperations.cpp +++ b/source/P4VFS.Core/Source/FileOperations.cpp @@ -1686,7 +1686,7 @@ HRESULT CreateProcessImpersonated( const WCHAR* commandLine, const WCHAR* currentDirectory, - BOOL waitForExit, + FileCore::Process::ExecuteFlags::Enum flags, FileCore::String* stdOutput, const FileCore::UserContext* context ) @@ -1702,11 +1702,6 @@ CreateProcessImpersonated( return HRESULT_FROM_WIN32(ERROR_INVALID_TOKEN); } - FileCore::Process::ExecuteFlags::Enum flags = FileCore::Process::ExecuteFlags::HideWindow; - if (waitForExit) - { - flags |= FileCore::Process::ExecuteFlags::WaitForExit; - } if (stdOutput != nullptr) { flags |= FileCore::Process::ExecuteFlags::StdOut; diff --git a/source/P4VFS.CoreInterop/Include/CoreInterop.h b/source/P4VFS.CoreInterop/Include/CoreInterop.h index 603be3e..9d15a7e 100644 --- a/source/P4VFS.CoreInterop/Include/CoreInterop.h +++ b/source/P4VFS.CoreInterop/Include/CoreInterop.h @@ -131,6 +131,18 @@ public ref class LogSystem abstract sealed ); }; +[System::FlagsAttribute] +public enum class ProcessExecuteFlags : System::Int32 +{ + None = FileCore::Process::ExecuteFlags::None, + WaitForExit = FileCore::Process::ExecuteFlags::WaitForExit, + HideWindow = FileCore::Process::ExecuteFlags::HideWindow, + StdOut = FileCore::Process::ExecuteFlags::StdOut, + KeepOpen = FileCore::Process::ExecuteFlags::KeepOpen, + Unelevated = FileCore::Process::ExecuteFlags::Unelevated, + Default = FileCore::Process::ExecuteFlags::Default, +}; + public ref class NativeMethods abstract sealed { public: @@ -219,7 +231,7 @@ public ref class NativeMethods abstract sealed CreateProcessImpersonated( System::String^ commandLine, System::String^ currentDirectory, - System::Boolean waitForExit, + ProcessExecuteFlags flags, System::Text::StringBuilder^ stdOutput, UserContext^ context ); diff --git a/source/P4VFS.CoreInterop/Source/CoreInterop.cpp b/source/P4VFS.CoreInterop/Source/CoreInterop.cpp index 978aebc..63bcc06 100644 --- a/source/P4VFS.CoreInterop/Source/CoreInterop.cpp +++ b/source/P4VFS.CoreInterop/Source/CoreInterop.cpp @@ -325,7 +325,7 @@ System::Boolean NativeMethods::CreateProcessImpersonated( System::String^ commandLine, System::String^ currentDirectory, - System::Boolean waitForExit, + ProcessExecuteFlags flags, System::Text::StringBuilder^ stdOutput, UserContext^ context ) @@ -334,7 +334,7 @@ NativeMethods::CreateProcessImpersonated( HRESULT status = FileOperations::CreateProcessImpersonated( marshal_as_wstring_c_str(commandLine), marshal_as_wstring_c_str(currentDirectory), - waitForExit, + static_cast(flags), stdOutput != nullptr ? &stdOutputResult : nullptr, marshal_as_user_context(context) ); diff --git a/source/P4VFS.Extensions/Source/Controls/LoginWindow.xaml.cs b/source/P4VFS.Extensions/Source/Controls/LoginWindow.xaml.cs index 5118dca..1307372 100644 --- a/source/P4VFS.Extensions/Source/Controls/LoginWindow.xaml.cs +++ b/source/P4VFS.Extensions/Source/Controls/LoginWindow.xaml.cs @@ -3,28 +3,28 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Xml; -using System.Threading.Tasks; +using System.Threading; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; +using System.Windows.Threading; namespace Microsoft.P4VFS.Extensions.Controls { public partial class LoginWindow : Window { + private CancellationToken? m_CancellationToken; + private DispatcherTimer m_UpdateTimer; + public LoginWindow() { InitializeComponent(); + + m_UpdateTimer = new DispatcherTimer(); + m_UpdateTimer.Interval = TimeSpan.FromSeconds(1.0/30.0); + m_UpdateTimer.Tick += OnTickUpdateTimer; + m_UpdateTimer.Start(); } - public string Port + public string Port { get { return m_Port.Text; } set { m_Port.Text = value; } @@ -48,6 +48,20 @@ public string Passwd set { m_Passwd.Password = value; } } + public CancellationToken? CancellationToken + { + get { return m_CancellationToken; } + set { m_CancellationToken = value; } + } + + private void OnTickUpdateTimer(object sender, EventArgs e) + { + if (m_CancellationToken?.IsCancellationRequested == true) + { + this.DialogResult = false; + } + } + private void OnClickButtonCancel(object sender, RoutedEventArgs e) { this.DialogResult = false; diff --git a/source/P4VFS.Extensions/Source/Utilities/ConsoleReader.cs b/source/P4VFS.Extensions/Source/Utilities/ConsoleReader.cs index bd79244..5c37953 100644 --- a/source/P4VFS.Extensions/Source/Utilities/ConsoleReader.cs +++ b/source/P4VFS.Extensions/Source/Utilities/ConsoleReader.cs @@ -5,188 +5,186 @@ using System.Linq; using System.Security; using System.Text; +using System.Threading; namespace Microsoft.P4VFS.Extensions.Utilities { - public static class ConsoleReader - { - private const char MaskCharacter = '*'; - - /// - /// Reads a single line of text from the system console, - /// masking the typed characters. - /// - public static string ReadLine() - { - StringBuilder buffer = new StringBuilder(64); - ReadLine(new StringBuffer(buffer), MaskCharacter); - return buffer.ToString(); - } - - /// - /// Reads a single line of text from the system console as a - /// SecureString instance, masking the typed - /// characters. - /// - /// - /// The caller is responsible for freeing the returned string - /// after usage by calling its Dispose() method. - /// - public static SecureString ReadSecureLine() - { - SecureString secureString = null; - try - { - secureString = new SecureString(); - ReadLine(new SecureStringBuffer(secureString), MaskCharacter); - secureString.MakeReadOnly(); - return secureString; - } - catch - { - if (secureString != null) - { - secureString.Dispose(); - } - throw; - } - } - - private static void ReadLine(IBuffer buffer, char maskChar) - { - int startPosition = Console.CursorLeft; - - int position = 0; - int length = 0; - - ConsoleKeyInfo keyInfo; - while ((keyInfo = Console.ReadKey(true)).Key != ConsoleKey.Enter) - { - if (keyInfo.Key == ConsoleKey.Backspace) - { - if (position > 0) - { - buffer.DeleteChar(--position); - Write(startPosition + --length, ' '); - } - } - else if (keyInfo.Key == ConsoleKey.UpArrow || - keyInfo.Key == ConsoleKey.PageUp || - keyInfo.Key == ConsoleKey.Escape) - { - // Match buffer-empty 'doskey' scenario for up - // arrow & page up. - buffer.Clear(); - for (; length >= 0; length--) - { - Write(startPosition + length, ' '); - } - position = length = 0; - } - else if (keyInfo.Key == ConsoleKey.DownArrow || - keyInfo.Key == ConsoleKey.PageDown) - { - // No-op. - } - else if (keyInfo.Key == ConsoleKey.LeftArrow) - { - position = Math.Max(position - 1, 0); - } - else if (keyInfo.Key == ConsoleKey.RightArrow) - { - position = Math.Min(position + 1, length); - } - else if (keyInfo.Key == ConsoleKey.Delete) - { - if (position < length) - { - buffer.DeleteChar(position); - Write(startPosition + --length, ' '); - } - } - else if (keyInfo.Key == ConsoleKey.Home) - { - position = 0; - } - else if (keyInfo.Key == ConsoleKey.End) - { - position = length; - } - else - { - buffer.InsertChar(position, keyInfo.KeyChar); - position++; - Write(startPosition + length, maskChar); - length++; - } - - Console.CursorLeft = position + startPosition; - } - - Console.WriteLine(); - } - - private static void Write(int index, char c) - { - Console.CursorLeft = index; - Console.Write(c); - } - - private interface IBuffer - { - void InsertChar(int index, char c); - void DeleteChar(int index); - void Clear(); - } - - private class StringBuffer : IBuffer - { - private StringBuilder buffer; - - public StringBuffer(StringBuilder buffer) - { - this.buffer = buffer; - } - - public void InsertChar(int index, char c) - { - this.buffer.Insert(index, c); - } - - public void DeleteChar(int index) - { - buffer.Remove(index, 1); - } - - public void Clear() - { - this.buffer.Length = 0; - } - } - - private class SecureStringBuffer : IBuffer - { - private SecureString buffer; - - public SecureStringBuffer(SecureString buffer) - { - this.buffer = buffer; - } - - public void InsertChar(int index, char c) - { - this.buffer.InsertAt(index, c); - } - - public void DeleteChar(int index) - { - this.buffer.RemoveAt(index); - } - - public void Clear() - { - this.buffer.Clear(); - } - } - } + public static class ConsoleReader + { + private const char MaskCharacter = '*'; + + /// + /// Reads a single line of text from the system console, + /// masking the typed characters. + /// + public static string ReadLine(CancellationToken cancellationToken = default) + { + StringBuilder buffer = new StringBuilder(64); + ReadLine(new StringBuffer(buffer), MaskCharacter, cancellationToken); + return buffer.ToString(); + } + + /// + /// Reads a single line of text from the system console as a + /// SecureString instance, masking the typed + /// characters. + /// + /// + /// The caller is responsible for freeing the returned string + /// after usage by calling its Dispose() method. + /// + public static SecureString ReadSecureLine(CancellationToken cancellationToken = default) + { + SecureString secureString = null; + try + { + secureString = new SecureString(); + ReadLine(new SecureStringBuffer(secureString), MaskCharacter, cancellationToken); + secureString.MakeReadOnly(); + return secureString; + } + catch + { + if (secureString != null) + { + secureString.Dispose(); + } + throw; + } + } + + private static void ReadLine(IBuffer buffer, char maskChar, CancellationToken cancellationToken = default) + { + int startPosition = Console.CursorLeft; + int position = 0; + int length = 0; + + ConsoleKeyInfo keyInfo; + while ((keyInfo = Console.ReadKey(true)).Key != ConsoleKey.Enter) + { + if (keyInfo.Key == ConsoleKey.Backspace) + { + if (position > 0) + { + buffer.DeleteChar(--position); + Write(startPosition + --length, ' '); + } + } + else if (keyInfo.Key == ConsoleKey.UpArrow || + keyInfo.Key == ConsoleKey.PageUp || + keyInfo.Key == ConsoleKey.Escape) + { + // Match buffer-empty 'doskey' scenario for up arrow & page up. + buffer.Clear(); + for (; length >= 0; length--) + { + Write(startPosition + length, ' '); + } + position = length = 0; + } + else if (keyInfo.Key == ConsoleKey.DownArrow || + keyInfo.Key == ConsoleKey.PageDown) + { + } + else if (keyInfo.Key == ConsoleKey.LeftArrow) + { + position = Math.Max(position - 1, 0); + } + else if (keyInfo.Key == ConsoleKey.RightArrow) + { + position = Math.Min(position + 1, length); + } + else if (keyInfo.Key == ConsoleKey.Delete) + { + if (position < length) + { + buffer.DeleteChar(position); + Write(startPosition + --length, ' '); + } + } + else if (keyInfo.Key == ConsoleKey.Home) + { + position = 0; + } + else if (keyInfo.Key == ConsoleKey.End) + { + position = length; + } + else + { + buffer.InsertChar(position, keyInfo.KeyChar); + position++; + Write(startPosition + length, maskChar); + length++; + } + + Console.CursorLeft = position + startPosition; + } + + Console.WriteLine(); + } + + private static void Write(int index, char c) + { + Console.CursorLeft = index; + Console.Write(c); + } + + private interface IBuffer + { + void InsertChar(int index, char c); + void DeleteChar(int index); + void Clear(); + } + + private class StringBuffer : IBuffer + { + private StringBuilder buffer; + + public StringBuffer(StringBuilder buffer) + { + this.buffer = buffer; + } + + public void InsertChar(int index, char c) + { + this.buffer.Insert(index, c); + } + + public void DeleteChar(int index) + { + buffer.Remove(index, 1); + } + + public void Clear() + { + this.buffer.Length = 0; + } + } + + private class SecureStringBuffer : IBuffer + { + private SecureString buffer; + + public SecureStringBuffer(SecureString buffer) + { + this.buffer = buffer; + } + + public void InsertChar(int index, char c) + { + this.buffer.InsertAt(index, c); + } + + public void DeleteChar(int index) + { + this.buffer.RemoveAt(index); + } + + public void Clear() + { + this.buffer.Clear(); + } + } + } } diff --git a/source/P4VFS.Extensions/Source/Utilities/ProcessInfo.cs b/source/P4VFS.Extensions/Source/Utilities/ProcessInfo.cs index 6207df0..a823b0e 100644 --- a/source/P4VFS.Extensions/Source/Utilities/ProcessInfo.cs +++ b/source/P4VFS.Extensions/Source/Utilities/ProcessInfo.cs @@ -29,10 +29,11 @@ public class ExecuteParams public bool UseShell = false; public bool ShowWindow = false; public bool AsAdmin = false; + public bool TerminateOnCancel = true; public string Input; public Action Output; public ProcessPriorityClass? Priority; - public CancellationToken? Token; + public CancellationToken? CancellationToken; public Dictionary Environment; } @@ -182,17 +183,17 @@ public static ExecuteResult ExecuteWait(ExecuteParams ep) List handleList = new List(); handleList.Add(process.Handle); - if (ep.Token != null) + if (ep.CancellationToken != null) { - handleList.Add(ep.Token.Value.WaitHandle.SafeWaitHandle.DangerousGetHandle()); + handleList.Add(ep.CancellationToken.Value.WaitHandle.SafeWaitHandle.DangerousGetHandle()); } IntPtr[] handles = handleList.ToArray(); WindowsInterop.WaitForMultipleObjects((uint)handles.Length, handles, false, WindowsInterop.INFINITE); - if (ep.Token != null) + if (ep.CancellationToken != null) { - ep.Token.Value.ThrowIfCancellationRequested(); + ep.CancellationToken.Value.ThrowIfCancellationRequested(); } process.WaitForExit(); @@ -205,7 +206,7 @@ public static ExecuteResult ExecuteWait(ExecuteParams ep) result.Exception = e; result.ExitCode = -1; result.WasCanceled = e is OperationCanceledException; - if (process.HasExited == false) + if (process.HasExited == false && ep.TerminateOnCancel) { result.WasKilled = true; process.Kill(); diff --git a/source/P4VFS.UnitTest/Source/UnitTestBase.cs b/source/P4VFS.UnitTest/Source/UnitTestBase.cs index 035b652..922c33d 100644 --- a/source/P4VFS.UnitTest/Source/UnitTestBase.cs +++ b/source/P4VFS.UnitTest/Source/UnitTestBase.cs @@ -336,6 +336,8 @@ public void WorkspaceReset(DepotConfig config = null) Assert(workspace.LineEnd == DepotResultClient.LineEnd.Local); Assert(workspace.Client != _P4Client || workspace.View.Count() == 1); Assert(workspace.Client != _P4Client || workspace.View.ElementAt(0) == String.Format("//depot/... //{0}/depot/...", workspace.Client)); + + UnitTestServer.ServerUninstallExtentions(config.Port); } Extensions.SocketModel.SocketModelClient service = new Extensions.SocketModel.SocketModelClient(); @@ -459,7 +461,7 @@ public static string GetExternalModuleFolder(string moduleName, bool required = return folderPath; } - public DepotConfig ClientConfig + public static DepotConfig ClientConfig { get { return new DepotConfig(){ Port = _P4Port, Client = _P4Client, User = _P4User }; } } diff --git a/source/P4VFS.UnitTest/Source/UnitTestCommon.cs b/source/P4VFS.UnitTest/Source/UnitTestCommon.cs index 8357ea3..5c90ba3 100644 --- a/source/P4VFS.UnitTest/Source/UnitTestCommon.cs +++ b/source/P4VFS.UnitTest/Source/UnitTestCommon.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.Linq; using System.IO; +using System.Net; using System.Xml; using System.Collections.Generic; +using System.Threading; using System.Text.RegularExpressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.P4VFS.Extensions; @@ -441,19 +443,19 @@ public void CreateProcessImpersonatedTest() { System.Text.StringBuilder output = new System.Text.StringBuilder(); string cmd = String.Format("\"{0}\\p4vfs.exe\" {1} login -w _incorrect_password_", System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), ClientConfig); - Assert(NativeMethods.CreateProcessImpersonated(cmd, null, true, output, null)); + Assert(NativeMethods.CreateProcessImpersonated(cmd, null, ProcessExecuteFlags.WaitForExit, output, null)); Assert(output.ToString().Split(new char[]{'\n','\r'}, StringSplitOptions.RemoveEmptyEntries).Contains("Login failed.")); } { System.Text.StringBuilder output = new System.Text.StringBuilder(); string cmd = String.Format("cmd.exe /s /c echo foobar"); - Assert(NativeMethods.CreateProcessImpersonated(cmd, null, true, output, null)); + Assert(NativeMethods.CreateProcessImpersonated(cmd, null, ProcessExecuteFlags.WaitForExit, output, null)); Assert(output.ToString().Split(new char[]{'\n','\r'}, StringSplitOptions.RemoveEmptyEntries).Contains("foobar")); } { System.Text.StringBuilder output = new System.Text.StringBuilder(); string cmd = String.Format("cmd.exe /s /c"); - Assert(NativeMethods.CreateProcessImpersonated(cmd, null, true, output, null)); + Assert(NativeMethods.CreateProcessImpersonated(cmd, null, ProcessExecuteFlags.WaitForExit, output, null)); Assert(output.ToString().Length == 0); } } @@ -551,20 +553,20 @@ public void MakeResidentRaceTest() Assert(IsPlaceholderFile(raceFile) == true); Random random = new Random(); - List workers = new List(); + List workers = new List(); List> workerArgs = new List>(); for (int workerIndex = 0; workerIndex < 20; ++workerIndex) { - System.Threading.Thread worker = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(p => + Thread worker = new Thread(new ParameterizedThreadStart(p => { try { Dictionary args = p as Dictionary; VirtualFileSystemLog.Verbose("MakeResidentRaceTest Process [{0}.{1}]", Process.GetCurrentProcess().Id, System.AppDomain.GetCurrentThreadId()); - System.Threading.Thread.Sleep((int)args["SleepTime"]); + Thread.Sleep((int)args["SleepTime"]); using (FileStream stream = File.Open(raceFile, FileMode.Open, FileAccess.Read, FileShare.Read)) { - System.Threading.Thread.Sleep(500); + Thread.Sleep(500); MemoryStream memStream = new MemoryStream(); stream.CopyTo(memStream); memStream.Seek(0, SeekOrigin.Begin); @@ -963,17 +965,17 @@ public void ParallelServiceTaskTest() Assert(IsPlaceholderFile(clientFile) == true); Random random = new Random(); - List workers = new List(); + List workers = new List(); List> workerArgs = new List>(); for (int workerIndex = 0; workerIndex < 32; ++workerIndex) { - System.Threading.Thread worker = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(p => + Thread worker = new Thread(new ParameterizedThreadStart(p => { try { Dictionary args = p as Dictionary; VirtualFileSystemLog.Verbose("ParallelServiceTaskTest Begin Process [{0}.{1}] -> {2}", Process.GetCurrentProcess().Id, System.AppDomain.GetCurrentThreadId(), args["ClientFile"]); - System.Threading.Thread.Sleep((int)args["SleepTime"]); + Thread.Sleep((int)args["SleepTime"]); using (FileStream stream = File.Open((string)args["ClientFile"], FileMode.Open, FileAccess.Read, FileShare.Read)) {} VirtualFileSystemLog.Verbose("ParallelServiceTaskTest End Process [{0}.{1}] -> {2}", Process.GetCurrentProcess().Id, System.AppDomain.GetCurrentThreadId(), args["ClientFile"]); Assert(IsPlaceholderFile((string)args["ClientFile"]) == false); @@ -2068,5 +2070,105 @@ public void SyncClientSizeTest() } }}} } + + [TestMethod, Priority(36)] + public void ShellLoginTimeoutTest() + { + WorkspaceReset(); + Assert(ShellUtilities.IsProcessElevated()); + + string workingFolder = String.Format("{0}\\{1}", UnitTestServer.GetServerRootFolder(), nameof(ShellLoginTimeoutTest)); + AssertLambda(() => FileUtilities.DeleteDirectoryAndFiles(workingFolder)); + AssertLambda(() => Directory.CreateDirectory(workingFolder)); + + string shellCommandFile = String.Format("{0}\\ShellCommand.bat", workingFolder); + string shellCommandUri = String.Format("file:///{0}", shellCommandFile.Replace('\\','/')); + Assert(Uri.IsWellFormedUriString(shellCommandUri, UriKind.Absolute)); + + var assertShellLogin = new Action((int cmdTimeout, int shellTimeout, bool native) => + { + VirtualFileSystemLog.Info($"assertShellLogin cmdTimeout={cmdTimeout} shellTimeout={shellTimeout} native={native}"); + FileUtilities.DeleteFile(shellCommandFile); + + string shellOutputFile = String.Format("{0}\\{1}-{2}.txt", workingFolder, Path.GetFileNameWithoutExtension(shellCommandFile), DateTime.Now.Ticks); + FileUtilities.DeleteFile(shellOutputFile); + + File.WriteAllLines(shellCommandFile, new string[]{ + $"fltmc.exe > {shellOutputFile}", + $"timeout.exe /nobreak {cmdTimeout} > nul 2>&1", + $"echo done >> {shellOutputFile}", + }); + + bool expectTimeout = cmdTimeout > shellTimeout; + System.Text.StringBuilder loginOutput = new System.Text.StringBuilder(); + string loginExe = P4vfsExe; + string loginArgs = String.Format("{0} login -t {1} -u {2}", ClientConfig, shellTimeout, shellCommandUri); + + if (native) + { + ProcessExecuteFlags flags = ProcessExecuteFlags.HideWindow | ProcessExecuteFlags.WaitForExit; + bool loginSuccess = NativeMethods.CreateProcessImpersonated($"{loginExe} {loginArgs}", null, flags, loginOutput, null); + Assert(loginSuccess); + } + else + { + int loginExitCode = ProcessInfo.ExecuteWait(loginExe, loginArgs, echo:true, stdout:loginOutput); + Assert(loginExitCode == 0); + } + + Assert(Regex.IsMatch(loginOutput.ToString(), "timeout waiting", RegexOptions.IgnoreCase) == expectTimeout); + Assert(File.Exists(shellOutputFile)); + Assert(Regex.IsMatch(File.ReadAllText(shellOutputFile), @"p4vfsflt\s+\d+\s+\d+")); + }); + + foreach (bool native in new[] { true, false }) + { + assertShellLogin(10, 20, native); + assertShellLogin(20, 10, native); + } + + string loginEndpoint = $"http://localhost:8099/"; + ManualResetEventSlim loginRequestEvent = new ManualResetEventSlim(); + HttpListener loginListener = new HttpListener(); + loginListener.Prefixes.Add(loginEndpoint); + loginListener.Start(); + + + Thread loginListenerThread = new Thread(new ThreadStart(() => + { + try + { + while (loginListener.IsListening) + { + HttpListenerContext context = loginListener.GetContext(); + VirtualFileSystemLog.Info($"ShellLoginTimeoutTest accepted HTTP request"); + context.Response.StatusCode = 200; + using (var writer = new StreamWriter(context.Response.OutputStream)) + { + writer.Write("Hello"); + } + context.Response.Close(); + loginRequestEvent.Set(); + } + } + catch {} + })); + + UnitTestServer.ServerInstallLoginHookExtension(loginEndpoint); + loginListenerThread.Start(); + + using (DepotClient depotClient = new DepotClient()) + { + Assert(depotClient.Connect(_P4Port, _P4Client, _P4User)); + depotClient.Login(); + } + + Assert(loginRequestEvent.Wait(TimeSpan.FromSeconds(10)), "Missing Http SSO login"); + + loginListener.Stop(); + loginListenerThread.Join(); + + UnitTestServer.ServerUninstallExtentions(); + } } } diff --git a/source/P4VFS.UnitTest/Source/UnitTestServer.cs b/source/P4VFS.UnitTest/Source/UnitTestServer.cs index 7647acc..ad28ede 100644 --- a/source/P4VFS.UnitTest/Source/UnitTestServer.cs +++ b/source/P4VFS.UnitTest/Source/UnitTestServer.cs @@ -734,6 +734,89 @@ public void CreateDuplicatePerforceServerTest() } } + public static void ServerInstallLoginHookExtension(string loginUrl) + { + string serverFolder = GetServerRootFolder(); + string loginHookName = "loginhook"; + string loginHookPackageFolder = Path.Combine(serverFolder, loginHookName); + AssertLambda(() => FileUtilities.DeleteDirectoryAndFiles(loginHookPackageFolder)); + AssertLambda(() => Directory.CreateDirectory(loginHookPackageFolder)); + + ProcessInfo.ExecuteWait(P4Exe, $"{ClientConfig} extension --delete Auth::{loginHookName} --yes", echo:true, log:true, directory:serverFolder); + FileUtilities.DeleteFile($"{serverFolder}\\{loginHookName}.p4-extension"); + + File.WriteAllLines($"{loginHookPackageFolder}\\main.lua", new[]{ + "function GlobalConfigFields()", + " return {}", + "end", + "function InstanceConfigFields()", + " return {}", + "end", + "function InstanceConfigEvents()", + " return {", + " [ \"auth-pre-sso\" ] = \"auth\",", + " [ \"auth-check-sso\" ] = \"auth\"", + " }", + "end", + "function AuthPreSSO()", + " Helix.Core.Server.log( \"P4VFS AuthPreSSO\" )", + " return true, \"unused\", \""+loginUrl+"\", false", + "end", + "function AuthCheckSSO()", + " Helix.Core.Server.log( \"P4VFS AuthCheckSSO\" )", + " return true", + "end" + }); + + File.WriteAllLines($"{loginHookPackageFolder}\\manifest.json", new[]{ + "{", + " \"manifest_version\": 1,", + " \"api_version\": 20191,", + " \"script_runtime\": { \"language\": \"Lua\", \"version\": \"5.3\" },", + " \"key\": \"117E9283-732B-45A6-9993-AE64C354F1C6\",", + " \"name\": \""+loginHookName+"\",", + " \"namespace\": \"Auth\",", + " \"version\": \"2019.1\",", + " \"version_name\": \"2019.1\",", + " \"description\": \"SSO auth integration\",", + " \"compatible_products\": [\"p4d\"],", + " \"default_locale\": \"en\",", + " \"supported_locales\": [\"en\"],", + " \"developer\": { \"name\": \"\", \"url\": \"\" },", + " \"homepage_url\": \"\",", + " \"license\": \"UNLICENSED\",", + " \"license_body\": \"UNLICENSED\"", + "}" + }); + + Assert(ProcessInfo.ExecuteWait(P4Exe, $"{ClientConfig} extension --package {loginHookName}", echo:true, log:true, directory:serverFolder) == 0); + Assert(ProcessInfo.ExecuteWait(P4Exe, $"{ClientConfig} extension --install {loginHookName}.p4-extension --yes --allow-unsigned", echo:true, log:true, directory:serverFolder) == 0); + + ProcessInfo.ExecuteResultOutput loginHookConfig = ProcessInfo.ExecuteWaitOutput(P4Exe, $"{ClientConfig} extension --configure Auth::{loginHookName} -o", echo:true, log:true, directory:serverFolder); + Assert(loginHookConfig.ExitCode == 0); + string loginHookConfigSpec = Regex.Replace(loginHookConfig.Text, @"^(ExtP4USER:\s+)(.+)$", $"$1{_P4User}", RegexOptions.Multiline); + Assert(ProcessInfo.ExecuteWait(P4Exe, $"{ClientConfig} extension --configure Auth::{loginHookName} -i", stdin:loginHookConfigSpec, echo:true, log:true, directory:serverFolder) == 0); + + ProcessInfo.ExecuteResultOutput loginHookInstConfig = ProcessInfo.ExecuteWaitOutput(P4Exe, $"{ClientConfig} extension --configure Auth::{loginHookName} --name Auth::{loginHookName}-instance -o", echo:true, log:true, directory:serverFolder); + Assert(loginHookInstConfig.ExitCode == 0); + Assert(ProcessInfo.ExecuteWait(P4Exe, $"{ClientConfig} extension --configure Auth::{loginHookName} --name Auth::{loginHookName}-instance -i", stdin:loginHookInstConfig.Text, echo:true, log:true, directory:serverFolder) == 0); + } + + public static void ServerUninstallExtentions(string p4Port = null) + { + using (DepotClient depotClient = new DepotClient()) + { + Assert(depotClient.Connect(p4Port ?? _P4Port, _P4Client, _P4User)); + foreach (DepotResultNode node in depotClient.Run("extension", new[]{"--list","--type","extensions"}).Nodes) + { + if (node.ContainsKey("extension")) + { + Assert(depotClient.Run("extension", new[]{"--delete",node.GetValue("extension"),"--yes"}).HasError == false); + } + } + } + } + private static void ServerWorkspaceReset(string p4Port = null) { string serverRootFolder = GetServerRootFolder(p4Port);