diff --git a/Scalar.Common/FileSystem/PhysicalFileSystem.cs b/Scalar.Common/FileSystem/PhysicalFileSystem.cs index 7740de4356..b6b23c6786 100644 --- a/Scalar.Common/FileSystem/PhysicalFileSystem.cs +++ b/Scalar.Common/FileSystem/PhysicalFileSystem.cs @@ -196,6 +196,11 @@ public virtual IEnumerable EnumerateDirectories(string path) return Directory.EnumerateDirectories(path); } + public virtual IEnumerable EnumerateFiles(string path, string searchPattern) + { + return Directory.EnumerateFiles(path, searchPattern); + } + public virtual FileProperties GetFileProperties(string path) { FileInfo entry = new FileInfo(path); diff --git a/Scalar.Common/NamedPipes/NamedPipeMessages.cs b/Scalar.Common/NamedPipes/NamedPipeMessages.cs index dde3aa5ac1..1322191826 100644 --- a/Scalar.Common/NamedPipes/NamedPipeMessages.cs +++ b/Scalar.Common/NamedPipes/NamedPipeMessages.cs @@ -116,82 +116,6 @@ public override string ToString() } } - public class UnregisterRepoRequest - { - public const string Header = nameof(UnregisterRepoRequest); - - public string EnlistmentRoot { get; set; } - - public static UnregisterRepoRequest FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - - public Message ToMessage() - { - return new Message(Header, JsonConvert.SerializeObject(this)); - } - - public class Response : BaseResponse - { - public static Response FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - } - } - - public class RegisterRepoRequest - { - public const string Header = nameof(RegisterRepoRequest); - - public string EnlistmentRoot { get; set; } - public string OwnerSID { get; set; } - - public static RegisterRepoRequest FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - - public Message ToMessage() - { - return new Message(Header, JsonConvert.SerializeObject(this)); - } - - public class Response : BaseResponse - { - public static Response FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - } - } - - public class GetActiveRepoListRequest - { - public const string Header = nameof(GetActiveRepoListRequest); - - public static GetActiveRepoListRequest FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - - public Message ToMessage() - { - return new Message(Header, JsonConvert.SerializeObject(this)); - } - - public class Response : BaseResponse - { - public List RepoList { get; set; } - - public static Response FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - } - } - public class BaseResponse { public const string Header = nameof(TRequest) + ResponseSuffix; diff --git a/Scalar.Common/Platforms/POSIX/POSIXFileSystem.Shared.cs b/Scalar.Common/Platforms/POSIX/POSIXFileSystem.Shared.cs index f6eb6c75dd..c8ff350073 100644 --- a/Scalar.Common/Platforms/POSIX/POSIXFileSystem.Shared.cs +++ b/Scalar.Common/Platforms/POSIX/POSIXFileSystem.Shared.cs @@ -4,7 +4,7 @@ public partial class POSIXFileSystem { public static bool TryGetNormalizedPathImplementation(string path, out string normalizedPath, out string errorMessage) { - // TODO(#1358): Properly determine normalized paths (e.g. across links) + // TODO(#217): Properly determine normalized paths (e.g. across links) errorMessage = null; normalizedPath = path; return true; diff --git a/Scalar.Common/RepoRegistry/IScalarRepoRegistry.cs b/Scalar.Common/RepoRegistry/IScalarRepoRegistry.cs new file mode 100644 index 0000000000..ad28eab592 --- /dev/null +++ b/Scalar.Common/RepoRegistry/IScalarRepoRegistry.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Scalar.Common.RepoRegistry +{ + public interface IScalarRepoRegistry + { + bool TryRegisterRepo(string normalizedRepoRoot, string userId, out string errorMessage); + bool TryUnregisterRepo(string normalizedRepoRoot, out string errorMessage); + IEnumerable GetRegisteredRepos(); + } +} diff --git a/Scalar.Common/RepoRegistry/ScalarRepoRegistration.cs b/Scalar.Common/RepoRegistry/ScalarRepoRegistration.cs new file mode 100644 index 0000000000..123e468a3d --- /dev/null +++ b/Scalar.Common/RepoRegistry/ScalarRepoRegistration.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Scalar.Common.RepoRegistry +{ + public class ScalarRepoRegistration + { + public ScalarRepoRegistration() + { + } + + public ScalarRepoRegistration(string normalizedRepoRoot, string userId) + { + this.NormalizedRepoRoot = normalizedRepoRoot; + this.UserId = userId; + } + + public string NormalizedRepoRoot { get; set; } + public string UserId { get; set; } + + public static ScalarRepoRegistration FromJson(string json) + { + return JsonConvert.DeserializeObject( + json, + new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore + }); + } + + public override string ToString() + { + return $"({this.UserId}) {this.NormalizedRepoRoot}"; + } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + } +} diff --git a/Scalar.Common/RepoRegistry/ScalarRepoRegistry.cs b/Scalar.Common/RepoRegistry/ScalarRepoRegistry.cs new file mode 100644 index 0000000000..4b3ada7828 --- /dev/null +++ b/Scalar.Common/RepoRegistry/ScalarRepoRegistry.cs @@ -0,0 +1,207 @@ +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; + +namespace Scalar.Common.RepoRegistry +{ + public class ScalarRepoRegistry : IScalarRepoRegistry + { + private const string EtwArea = nameof(ScalarRepoRegistry); + private const string RegistryFileExtension = ".repo"; + private const string RegistryTempFileExtension = ".temp"; + + private string registryFolderPath; + private ITracer tracer; + private PhysicalFileSystem fileSystem; + + public ScalarRepoRegistry( + ITracer tracer, + PhysicalFileSystem fileSystem, + string repoRegistryLocation) + { + this.tracer = tracer; + this.fileSystem = fileSystem; + this.registryFolderPath = repoRegistryLocation; + } + + public bool TryRegisterRepo(string normalizedRepoRoot, string userId, out string errorMessage) + { + try + { + if (!this.fileSystem.DirectoryExists(this.registryFolderPath)) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add(nameof(this.registryFolderPath), this.registryFolderPath); + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.TryRegisterRepo)}_CreatingRegistryDirectory", + metadata); + + // TODO #136: Make sure this does the right thing with ACLs on Windows + this.fileSystem.CreateDirectory(this.registryFolderPath); + } + } + catch (Exception e) + { + errorMessage = $"Error while ensuring registry directory '{this.registryFolderPath}' exists: {e.Message}"; + + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add(nameof(normalizedRepoRoot), normalizedRepoRoot); + metadata.Add(nameof(this.registryFolderPath), this.registryFolderPath); + this.tracer.RelatedError(metadata, $"{nameof(this.TryRegisterRepo)}: Exception while ensuring registry directory exists"); + return false; + } + + string tempRegistryPath = this.GetRepoRegistryTempFilePath(normalizedRepoRoot); + + try + { + ScalarRepoRegistration repoRegistration = new ScalarRepoRegistration(normalizedRepoRoot, userId); + string registryFileContents = repoRegistration.ToJson(); + + using (Stream tempFile = this.fileSystem.OpenFileStream( + tempRegistryPath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + callFlushFileBuffers: true)) + using (StreamWriter writer = new StreamWriter(tempFile)) + { + writer.WriteLine(registryFileContents); + tempFile.Flush(); + } + } + catch (Exception e) + { + errorMessage = $"Error while registering repo {normalizedRepoRoot}: {e.Message}"; + + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add(nameof(normalizedRepoRoot), normalizedRepoRoot); + metadata.Add(nameof(tempRegistryPath), tempRegistryPath); + this.tracer.RelatedError(metadata, $"{nameof(this.TryRegisterRepo)}: Exception while writing temp registry file"); + return false; + } + + string registryFilePath = this.GetRepoRegistryFilePath(normalizedRepoRoot); + try + { + this.fileSystem.MoveAndOverwriteFile(tempRegistryPath, registryFilePath); + } + catch (Win32Exception e) + { + errorMessage = $"Error while registering repo {normalizedRepoRoot}: {e.Message}"; + + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add(nameof(normalizedRepoRoot), normalizedRepoRoot); + metadata.Add(nameof(tempRegistryPath), tempRegistryPath); + metadata.Add(nameof(registryFilePath), registryFilePath); + this.tracer.RelatedError(metadata, $"{nameof(this.TryRegisterRepo)}: Exception while renaming temp registry file"); + return false; + } + + errorMessage = null; + return true; + } + + public bool TryUnregisterRepo(string normalizedRepoRoot, out string errorMessage) + { + string registryPath = this.GetRepoRegistryFilePath(normalizedRepoRoot); + if (!this.fileSystem.FileExists(registryPath)) + { + errorMessage = $"Attempted to remove non-existent repo '{normalizedRepoRoot}'"; + + EventMetadata metadata = CreateEventMetadata(); + metadata.Add(nameof(normalizedRepoRoot), normalizedRepoRoot); + metadata.Add(nameof(registryPath), registryPath); + this.tracer.RelatedWarning( + metadata, + $"{nameof(this.TryUnregisterRepo)}: Attempted to remove non-existent repo"); + + return false; + } + + try + { + this.fileSystem.DeleteFile(registryPath); + } + catch (Exception e) + { + errorMessage = $"Error while removing repo {normalizedRepoRoot}: {e.Message}"; + + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add(nameof(normalizedRepoRoot), normalizedRepoRoot); + metadata.Add(nameof(registryPath), registryPath); + this.tracer.RelatedWarning( + metadata, + $"{nameof(this.TryUnregisterRepo)}: Exception while removing repo"); + + return false; + } + + errorMessage = null; + return true; + } + + public IEnumerable GetRegisteredRepos() + { + if (this.fileSystem.DirectoryExists(this.registryFolderPath)) + { + IEnumerable registryFilePaths = this.fileSystem.EnumerateFiles(this.registryFolderPath, $"*{RegistryFileExtension}"); + foreach (string registryFilePath in registryFilePaths) + { + ScalarRepoRegistration registration = null; + try + { + string repoData = this.fileSystem.ReadAllText(registryFilePath); + registration = ScalarRepoRegistration.FromJson(repoData); + } + catch (Exception e) + { + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add(nameof(registryFilePath), registryFilePath); + this.tracer.RelatedWarning( + metadata, + $"{nameof(this.GetRegisteredRepos)}: Failed to read registry file"); + } + + if (registration != null) + { + yield return registration; + } + } + } + } + + internal static string GetRepoRootSha(string normalizedRepoRoot) + { + return SHA1Util.SHA1HashStringForUTF8String(normalizedRepoRoot.ToLowerInvariant()); + } + + private static EventMetadata CreateEventMetadata(Exception e = null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + private string GetRepoRegistryTempFilePath(string normalizedRepoRoot) + { + string repoTempFilename = $"{GetRepoRootSha(normalizedRepoRoot)}{RegistryTempFileExtension}"; + return Path.Combine(this.registryFolderPath, repoTempFilename); + } + + private string GetRepoRegistryFilePath(string normalizedRepoRoot) + { + string repoFilename = $"{GetRepoRootSha(normalizedRepoRoot)}{RegistryFileExtension}"; + return Path.Combine(this.registryFolderPath, repoFilename); + } + } +} diff --git a/Scalar.Common/ScalarConstants.cs b/Scalar.Common/ScalarConstants.cs index 8972068a10..a265801b9a 100644 --- a/Scalar.Common/ScalarConstants.cs +++ b/Scalar.Common/ScalarConstants.cs @@ -54,6 +54,11 @@ public static class Service public const string UIName = "Scalar.Service.UI"; } + public static class RepoRegistry + { + public const string RegistryDirectoryName = "Scalar.RepoRegistry"; + } + public static class MediaTypes { public const string PrefetchPackFilesAndIndexesMediaType = "application/x-gvfs-timestamped-packfiles-indexes"; diff --git a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs index ad65ff72ec..c36ac1f9c6 100644 --- a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs @@ -5,24 +5,11 @@ namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests { [TestFixture] - [NonParallelizable] [Category(Categories.ExtraCoverage)] - [Category(Categories.MacTODO.NeedsServiceVerb)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] public class ServiceVerbTests : TestsWithMultiEnlistment { - private static readonly string[] EmptyRepoList = new string[] { }; - [TestCase] - public void ServiceCommandsWithNoRepos() - { - this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList); - this.RunServiceCommandAndCheckOutput("--mount-all", EmptyRepoList); - this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList); - } - - [TestCase] - public void ServiceCommandsWithMultipleRepos() + public void ServiceListRegistered() { ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(); ScalarFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(); @@ -39,51 +26,9 @@ public void ServiceCommandsWithMultipleRepos() enlistment2.EnlistmentRoot, enlistment2.LocalCacheRoot); - this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList); - this.RunServiceCommandAndCheckOutput("--unmount-all", expectedRepoRoots: repoRootList); - - // Check both are unmounted - scalarProcess1.IsEnlistmentMounted().ShouldEqual(false); - scalarProcess2.IsEnlistmentMounted().ShouldEqual(false); - - this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList); - this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList); - this.RunServiceCommandAndCheckOutput("--mount-all", expectedRepoRoots: repoRootList); - - // Check both are mounted - scalarProcess1.IsEnlistmentMounted().ShouldEqual(true); - scalarProcess2.IsEnlistmentMounted().ShouldEqual(true); - - this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList); - } - - [TestCase] - public void ServiceCommandsWithMountAndUnmount() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(); - - string[] repoRootList = new string[] { enlistment1.EnlistmentRoot }; - - ScalarProcess scalarProcess1 = new ScalarProcess( - ScalarTestConfig.PathToScalar, - enlistment1.EnlistmentRoot, - enlistment1.LocalCacheRoot); - - this.RunServiceCommandAndCheckOutput("--list-mounted", expectedRepoRoots: repoRootList); - - scalarProcess1.Unmount(); - - this.RunServiceCommandAndCheckOutput("--list-mounted", EmptyRepoList, unexpectedRepoRoots: repoRootList); - this.RunServiceCommandAndCheckOutput("--unmount-all", EmptyRepoList, unexpectedRepoRoots: repoRootList); - this.RunServiceCommandAndCheckOutput("--mount-all", EmptyRepoList, unexpectedRepoRoots: repoRootList); - - // Check that it is still unmounted - scalarProcess1.IsEnlistmentMounted().ShouldEqual(false); - - scalarProcess1.Mount(); - - this.RunServiceCommandAndCheckOutput("--unmount-all", expectedRepoRoots: repoRootList); - this.RunServiceCommandAndCheckOutput("--mount-all", expectedRepoRoots: repoRootList); + // Do not check for unexpected repos, as other repos on the machine may be registered while + // this test is running + this.RunServiceCommandAndCheckOutput("--list-registered", expectedRepoRoots: repoRootList); } private void RunServiceCommandAndCheckOutput(string argument, string[] expectedRepoRoots, string[] unexpectedRepoRoots = null) diff --git a/Scalar.Service/Handlers/GetActiveRepoListHandler.cs b/Scalar.Service/Handlers/GetActiveRepoListHandler.cs deleted file mode 100644 index 78c4ff6302..0000000000 --- a/Scalar.Service/Handlers/GetActiveRepoListHandler.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Scalar.Common; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System.Collections.Generic; -using System.Linq; - -namespace Scalar.Service.Handlers -{ - public class GetActiveRepoListHandler : MessageHandler - { - private NamedPipeServer.Connection connection; - private NamedPipeMessages.GetActiveRepoListRequest request; - private ITracer tracer; - private IRepoRegistry registry; - - public GetActiveRepoListHandler( - ITracer tracer, - IRepoRegistry registry, - NamedPipeServer.Connection connection, - NamedPipeMessages.GetActiveRepoListRequest request) - { - this.tracer = tracer; - this.registry = registry; - this.connection = connection; - this.request = request; - } - - public void Run() - { - string errorMessage; - NamedPipeMessages.GetActiveRepoListRequest.Response response = new NamedPipeMessages.GetActiveRepoListRequest.Response(); - response.State = NamedPipeMessages.CompletionState.Success; - response.RepoList = new List(); - - List repos; - if (this.registry.TryGetActiveRepos(out repos, out errorMessage)) - { - List tempRepoList = repos.Select(repo => repo.EnlistmentRoot).ToList(); - - foreach (string repoRoot in tempRepoList) - { - if (!this.IsValidRepo(repoRoot)) - { - if (!this.registry.TryRemoveRepo(repoRoot, out errorMessage)) - { - this.tracer.RelatedInfo("Removing an invalid repo failed with error: " + response.ErrorMessage); - } - else - { - this.tracer.RelatedInfo("Removed invalid repo entry from registry: " + repoRoot); - } - } - else - { - response.RepoList.Add(repoRoot); - } - } - } - else - { - response.ErrorMessage = errorMessage; - response.State = NamedPipeMessages.CompletionState.Failure; - this.tracer.RelatedError("Get active repo list failed with error: " + response.ErrorMessage); - } - - this.WriteToClient(response.ToMessage(), this.connection, this.tracer); - } - - private bool IsValidRepo(string repoRoot) - { - string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - try - { - ScalarEnlistment enlistment = ScalarEnlistment.CreateFromDirectory( - repoRoot, - gitBinPath, - authentication: null); - } - catch (InvalidRepoException e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(repoRoot), repoRoot); - metadata.Add(nameof(gitBinPath), gitBinPath); - metadata.Add("Exception", e.ToString()); - this.tracer.RelatedInfo(metadata, $"{nameof(this.IsValidRepo)}: Found invalid repo"); - - return false; - } - - return true; - } - } -} diff --git a/Scalar.Service/Handlers/MessageHandler.cs b/Scalar.Service/Handlers/MessageHandler.cs deleted file mode 100644 index e6289b50a8..0000000000 --- a/Scalar.Service/Handlers/MessageHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; - -namespace Scalar.Service.Handlers -{ - public abstract class MessageHandler - { - protected void WriteToClient(NamedPipeMessages.Message message, NamedPipeServer.Connection connection, ITracer tracer) - { - if (!connection.TrySendResponse(message)) - { - tracer.RelatedError("Failed to send line to client: {0}", message); - } - } - } -} diff --git a/Scalar.Service/Handlers/RegisterRepoHandler.cs b/Scalar.Service/Handlers/RegisterRepoHandler.cs deleted file mode 100644 index 744028e83b..0000000000 --- a/Scalar.Service/Handlers/RegisterRepoHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; - -namespace Scalar.Service.Handlers -{ - public class RegisterRepoHandler : MessageHandler - { - private NamedPipeServer.Connection connection; - private NamedPipeMessages.RegisterRepoRequest request; - private ITracer tracer; - private IRepoRegistry registry; - - public RegisterRepoHandler( - ITracer tracer, - IRepoRegistry registry, - NamedPipeServer.Connection connection, - NamedPipeMessages.RegisterRepoRequest request) - { - this.tracer = tracer; - this.registry = registry; - this.connection = connection; - this.request = request; - } - - public void Run() - { - string errorMessage = string.Empty; - NamedPipeMessages.RegisterRepoRequest.Response response = new NamedPipeMessages.RegisterRepoRequest.Response(); - - if (this.registry.TryRegisterRepo(this.request.EnlistmentRoot, this.request.OwnerSID, out errorMessage)) - { - response.State = NamedPipeMessages.CompletionState.Success; - this.tracer.RelatedInfo("Registered repo {0}", this.request.EnlistmentRoot); - } - else - { - response.ErrorMessage = errorMessage; - response.State = NamedPipeMessages.CompletionState.Failure; - this.tracer.RelatedError("Failed to register repo {0} with error: {1}", this.request.EnlistmentRoot, errorMessage); - } - - this.WriteToClient(response.ToMessage(), this.connection, this.tracer); - } - } -} diff --git a/Scalar.Service/Handlers/RequestHandler.cs b/Scalar.Service/Handlers/RequestHandler.cs index 38c76e0965..63015922b4 100644 --- a/Scalar.Service/Handlers/RequestHandler.cs +++ b/Scalar.Service/Handlers/RequestHandler.cs @@ -7,29 +7,20 @@ namespace Scalar.Service.Handlers /// /// RequestHandler - Routes client requests that reach Scalar.Service to /// appropriate MessageHandler object. - /// Example requests - scalar mount/unmount command sends requests to - /// register/un-register repositories for automount. RequestHandler - /// routes them to RegisterRepoHandler and UnRegisterRepoHandler - /// respectively. /// public class RequestHandler { protected string requestDescription; - private const string MountRequestDescription = "mount"; - private const string UnmountRequestDescription = "unmount"; - private const string RepoListRequestDescription = "repo list"; private const string UnknownRequestDescription = "unknown"; private string etwArea; private ITracer tracer; - private IRepoRegistry repoRegistry; - public RequestHandler(ITracer tracer, string etwArea, IRepoRegistry repoRegistry) + public RequestHandler(ITracer tracer, string etwArea) { this.tracer = tracer; this.etwArea = etwArea; - this.repoRegistry = repoRegistry; } public void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) @@ -65,30 +56,6 @@ protected virtual void HandleMessage( { switch (message.Header) { - case NamedPipeMessages.RegisterRepoRequest.Header: - this.requestDescription = MountRequestDescription; - NamedPipeMessages.RegisterRepoRequest mountRequest = NamedPipeMessages.RegisterRepoRequest.FromMessage(message); - RegisterRepoHandler mountHandler = new RegisterRepoHandler(tracer, this.repoRegistry, connection, mountRequest); - mountHandler.Run(); - - break; - - case NamedPipeMessages.UnregisterRepoRequest.Header: - this.requestDescription = UnmountRequestDescription; - NamedPipeMessages.UnregisterRepoRequest unmountRequest = NamedPipeMessages.UnregisterRepoRequest.FromMessage(message); - UnregisterRepoHandler unmountHandler = new UnregisterRepoHandler(tracer, this.repoRegistry, connection, unmountRequest); - unmountHandler.Run(); - - break; - - case NamedPipeMessages.GetActiveRepoListRequest.Header: - this.requestDescription = RepoListRequestDescription; - NamedPipeMessages.GetActiveRepoListRequest repoListRequest = NamedPipeMessages.GetActiveRepoListRequest.FromMessage(message); - GetActiveRepoListHandler excludeHandler = new GetActiveRepoListHandler(tracer, this.repoRegistry, connection, repoListRequest); - excludeHandler.Run(); - - break; - default: this.requestDescription = UnknownRequestDescription; EventMetadata metadata = new EventMetadata(); diff --git a/Scalar.Service/Handlers/UnregisterRepoHandler.cs b/Scalar.Service/Handlers/UnregisterRepoHandler.cs deleted file mode 100644 index 749fc158ee..0000000000 --- a/Scalar.Service/Handlers/UnregisterRepoHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; - -namespace Scalar.Service.Handlers -{ - public class UnregisterRepoHandler : MessageHandler - { - private NamedPipeServer.Connection connection; - private NamedPipeMessages.UnregisterRepoRequest request; - private ITracer tracer; - private IRepoRegistry registry; - - public UnregisterRepoHandler( - ITracer tracer, - IRepoRegistry registry, - NamedPipeServer.Connection connection, - NamedPipeMessages.UnregisterRepoRequest request) - { - this.tracer = tracer; - this.registry = registry; - this.connection = connection; - this.request = request; - } - - public void Run() - { - string errorMessage = string.Empty; - NamedPipeMessages.UnregisterRepoRequest.Response response = new NamedPipeMessages.UnregisterRepoRequest.Response(); - - if (this.registry.TryDeactivateRepo(this.request.EnlistmentRoot, out errorMessage)) - { - response.State = NamedPipeMessages.CompletionState.Success; - this.tracer.RelatedInfo("Deactivated repo {0}", this.request.EnlistmentRoot); - } - else - { - response.ErrorMessage = errorMessage; - response.State = NamedPipeMessages.CompletionState.Failure; - this.tracer.RelatedError("Failed to deactivate repo {0} with error: {1}", this.request.EnlistmentRoot, errorMessage); - } - - this.WriteToClient(response.ToMessage(), this.connection, this.tracer); - } - } -} diff --git a/Scalar.Service/IRepoRegistry.cs b/Scalar.Service/IRepoRegistry.cs deleted file mode 100644 index 38595863ca..0000000000 --- a/Scalar.Service/IRepoRegistry.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Scalar.Common.Maintenance; - -namespace Scalar.Service -{ - public interface IRepoRegistry - { - bool TryRegisterRepo(string repoRoot, string ownerSID, out string errorMessage); - bool TryDeactivateRepo(string repoRoot, out string errorMessage); - bool TryGetActiveRepos(out List repoList, out string errorMessage); - bool TryRemoveRepo(string repoRoot, out string errorMessage); - void RunMaintenanceTaskForRepos(MaintenanceTasks.Task task, string userId, int sessionId); - void TraceStatus(); - } -} diff --git a/Scalar.Service/InternalsVisibleTo.cs b/Scalar.Service/InternalsVisibleTo.cs new file mode 100644 index 0000000000..ac7bc57a09 --- /dev/null +++ b/Scalar.Service/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Scalar.UnitTests")] diff --git a/Scalar.Service/MacScalarService.cs b/Scalar.Service/MacScalarService.cs index 3a0d01b6a2..5761a75702 100644 --- a/Scalar.Service/MacScalarService.cs +++ b/Scalar.Service/MacScalarService.cs @@ -1,5 +1,7 @@ using Scalar.Common; +using Scalar.Common.FileSystem; using Scalar.Common.NamedPipes; +using Scalar.Common.RepoRegistry; using Scalar.Common.Tracing; using Scalar.Service.Handlers; using System; @@ -17,14 +19,14 @@ public class MacScalarService private Thread serviceThread; private ManualResetEvent serviceStopped; private string serviceName; - private IRepoRegistry repoRegistry; + private IScalarRepoRegistry repoRegistry; private RequestHandler requestHandler; private MaintenanceTaskScheduler maintenanceTaskScheduler; public MacScalarService( ITracer tracer, string serviceName, - IRepoRegistry repoRegistry) + IScalarRepoRegistry repoRegistry) { this.tracer = tracer; this.repoRegistry = repoRegistry; @@ -32,7 +34,7 @@ public MacScalarService( this.serviceStopped = new ManualResetEvent(false); this.serviceThread = new Thread(this.ServiceThreadMain); - this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); + this.requestHandler = new RequestHandler(this.tracer, EtwArea); } public void Run() @@ -91,7 +93,11 @@ private void ServiceThreadMain() { try { - this.maintenanceTaskScheduler = new MaintenanceTaskScheduler(this.tracer, this.repoRegistry); + this.maintenanceTaskScheduler = new MaintenanceTaskScheduler( + this.tracer, + new PhysicalFileSystem(), + new MacScalarVerbRunner(this.tracer), + this.repoRegistry); // On Mac, there is no separate session Id. currentUser is used as sessionId this.maintenanceTaskScheduler.RegisterUser(new UserAndSession(currentUser, sessionId)); diff --git a/Scalar.Service/MaintenanceTaskScheduler.cs b/Scalar.Service/MaintenanceTaskScheduler.cs index eeb8b93b23..e97d10deef 100644 --- a/Scalar.Service/MaintenanceTaskScheduler.cs +++ b/Scalar.Service/MaintenanceTaskScheduler.cs @@ -1,8 +1,12 @@ using Scalar.Common; +using Scalar.Common.FileSystem; using Scalar.Common.Maintenance; +using Scalar.Common.RepoRegistry; using Scalar.Common.Tracing; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; namespace Scalar.Service @@ -21,15 +25,25 @@ public class MaintenanceTaskScheduler : IDisposable, IRegisteredUserStore private readonly TimeSpan fetchCommitsAndTreesPeriod = TimeSpan.FromMinutes(15); private readonly ITracer tracer; + private readonly PhysicalFileSystem fileSystem; + private readonly IScalarVerbRunner scalarVerb; + private readonly IScalarRepoRegistry repoRegistry; private ServiceTaskQueue taskQueue; private List taskTimers; - public MaintenanceTaskScheduler(ITracer tracer, IRepoRegistry repoRegistry) + public MaintenanceTaskScheduler( + ITracer tracer, + PhysicalFileSystem fileSystem, + IScalarVerbRunner scalarVerb, + IScalarRepoRegistry repoRegistry) { this.tracer = tracer; + this.fileSystem = fileSystem; + this.scalarVerb = scalarVerb; + this.repoRegistry = repoRegistry; this.taskTimers = new List(); this.taskQueue = new ServiceTaskQueue(this.tracer); - this.ScheduleRecurringTasks(repoRegistry); + this.ScheduleRecurringTasks(); } public UserAndSession RegisteredUser { get; private set; } @@ -64,7 +78,7 @@ public void Dispose() this.taskTimers = null; } - private void ScheduleRecurringTasks(IRepoRegistry repoRegistry) + private void ScheduleRecurringTasks() { if (ScalarEnlistment.IsUnattended(this.tracer)) { @@ -98,7 +112,9 @@ private void ScheduleRecurringTasks(IRepoRegistry repoRegistry) (state) => this.taskQueue.TryEnqueue( new MaintenanceTask( this.tracer, - repoRegistry, + this.fileSystem, + this.scalarVerb, + this.repoRegistry, this, schedule.Task)), state: null, @@ -107,34 +123,26 @@ private void ScheduleRecurringTasks(IRepoRegistry repoRegistry) } } - private class MaintenanceSchedule + internal class MaintenanceTask : IServiceTask { - public MaintenanceSchedule(MaintenanceTasks.Task task, TimeSpan dueTime, TimeSpan period) - { - this.Task = task; - this.DueTime = dueTime; - this.Period = period; - } - - public MaintenanceTasks.Task Task { get; } - public TimeSpan DueTime { get; } - public TimeSpan Period { get; } - } - - private class MaintenanceTask : IServiceTask - { - private readonly MaintenanceTasks.Task task; - private readonly IRepoRegistry repoRegistry; private readonly ITracer tracer; + private readonly PhysicalFileSystem fileSystem; + private readonly IScalarVerbRunner scalarVerb; + private readonly IScalarRepoRegistry repoRegistry; private readonly IRegisteredUserStore registeredUserStore; + private readonly MaintenanceTasks.Task task; public MaintenanceTask( ITracer tracer, - IRepoRegistry repoRegistry, + PhysicalFileSystem fileSystem, + IScalarVerbRunner scalarVerb, + IScalarRepoRegistry repoRegistry, IRegisteredUserStore registeredUserStore, MaintenanceTasks.Task task) { this.tracer = tracer; + this.fileSystem = fileSystem; + this.scalarVerb = scalarVerb; this.repoRegistry = repoRegistry; this.registeredUserStore = registeredUserStore; this.task = task; @@ -148,17 +156,12 @@ public void Execute() EventMetadata metadata = new EventMetadata(); metadata.Add(nameof(registeredUser.UserId), registeredUser.UserId); metadata.Add(nameof(registeredUser.SessionId), registeredUser.SessionId); - metadata.Add(nameof(this.task), this.task); + metadata.Add(nameof(this.task), this.task.ToString()); metadata.Add(TracingConstants.MessageKey.InfoMessage, "Executing maintenance task"); - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(MaintenanceTaskScheduler)}.{nameof(this.Execute)}", - metadata); - - this.repoRegistry.RunMaintenanceTaskForRepos( - this.task, - registeredUser.UserId, - registeredUser.SessionId); + using (ITracer activity = this.tracer.StartActivity($"{nameof(MaintenanceTask)}.{nameof(this.Execute)}", EventLevel.Informational, metadata)) + { + this.RunMaintenanceTaskForRepos(registeredUser); + } } else { @@ -170,6 +173,107 @@ public void Stop() { // TODO: #185 - Kill the currently running maintenance verb } + + private void RunMaintenanceTaskForRepos(UserAndSession registeredUser) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(this.task), MaintenanceTasks.GetVerbTaskName(this.task)); + metadata.Add(nameof(registeredUser.UserId), registeredUser.UserId); + metadata.Add(nameof(registeredUser.SessionId), registeredUser.SessionId); + + int reposSkipped = 0; + int reposSuccessfullyRemoved = 0; + int repoRemovalFailures = 0; + int reposMaintained = 0; + int reposInRegistryForUser = 0; + + string rootPath; + string errorMessage; + + IEnumerable reposForUser = this.repoRegistry.GetRegisteredRepos().Where( + x => x.UserId.Equals(registeredUser.UserId, StringComparison.InvariantCultureIgnoreCase)); + + foreach (ScalarRepoRegistration repoRegistration in reposForUser) + { + ++reposInRegistryForUser; + rootPath = Path.GetPathRoot(repoRegistration.NormalizedRepoRoot); + + metadata[nameof(repoRegistration.NormalizedRepoRoot)] = repoRegistration.NormalizedRepoRoot; + metadata[nameof(rootPath)] = rootPath; + metadata.Remove(nameof(errorMessage)); + + if (!string.IsNullOrWhiteSpace(rootPath) && !this.fileSystem.DirectoryExists(rootPath)) + { + ++reposSkipped; + + // If the volume does not exist we'll assume the drive was removed or is encrypted, + // and we'll leave the repo in the registry (but we won't run maintenance on it). + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.RunMaintenanceTaskForRepos)}_SkippedRepoWithMissingVolume", + metadata); + + continue; + } + + if (!this.fileSystem.DirectoryExists(repoRegistration.NormalizedRepoRoot)) + { + // The repo is no longer on disk (but its volume is present) + // Unregister the repo + if (this.repoRegistry.TryUnregisterRepo(repoRegistration.NormalizedRepoRoot, out errorMessage)) + { + ++reposSuccessfullyRemoved; + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.RunMaintenanceTaskForRepos)}_RemovedMissingRepo", + metadata); + } + else + { + ++repoRemovalFailures; + metadata[nameof(errorMessage)] = errorMessage; + this.tracer.RelatedEvent( + EventLevel.Warning, + $"{nameof(this.RunMaintenanceTaskForRepos)}_FailedToRemoveRepo", + metadata); + } + + continue; + } + + ++reposMaintained; + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.RunMaintenanceTaskForRepos)}_CallingMaintenance", + metadata); + + this.scalarVerb.CallMaintenance(this.task, repoRegistration.NormalizedRepoRoot, registeredUser.SessionId); + } + + metadata.Add(nameof(reposInRegistryForUser), reposInRegistryForUser); + metadata.Add(nameof(reposSkipped), reposSkipped); + metadata.Add(nameof(reposSuccessfullyRemoved), reposSuccessfullyRemoved); + metadata.Add(nameof(repoRemovalFailures), repoRemovalFailures); + metadata.Add(nameof(reposMaintained), reposMaintained); + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.RunMaintenanceTaskForRepos)}_MaintenanceSummary", + metadata); + } + } + + private class MaintenanceSchedule + { + public MaintenanceSchedule(MaintenanceTasks.Task task, TimeSpan dueTime, TimeSpan period) + { + this.Task = task; + this.DueTime = dueTime; + this.Period = period; + } + + public MaintenanceTasks.Task Task { get; } + public TimeSpan DueTime { get; } + public TimeSpan Period { get; } } } } diff --git a/Scalar.Service/Program.cs b/Scalar.Service/Program.cs index b7714bd552..b5be46b0c9 100644 --- a/Scalar.Service/Program.cs +++ b/Scalar.Service/Program.cs @@ -1,5 +1,6 @@ using Scalar.Common; using Scalar.Common.FileSystem; +using Scalar.Common.RepoRegistry; using Scalar.Common.Tracing; using Scalar.PlatformLoader; using Scalar.Service.Handlers; @@ -68,12 +69,11 @@ private static MacScalarService CreateMacService(JsonTracer tracer, string[] arg EventLevel.Informational, Keywords.Any); - string serviceDataLocation = scalarPlatform.GetDataRootForScalarComponent(serviceName); - RepoRegistry repoRegistry = new RepoRegistry( + string repoRegistryLocation = scalarPlatform.GetDataRootForScalarComponent(ScalarConstants.RepoRegistry.RegistryDirectoryName); + ScalarRepoRegistry repoRegistry = new ScalarRepoRegistry( tracer, new PhysicalFileSystem(), - serviceDataLocation, - new MacScalarVerbRunner(tracer)); + repoRegistryLocation); return new MacScalarService(tracer, serviceName, repoRegistry); } diff --git a/Scalar.Service/RepoRegistration.cs b/Scalar.Service/RepoRegistration.cs deleted file mode 100644 index e175725161..0000000000 --- a/Scalar.Service/RepoRegistration.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json; - -namespace Scalar.Service -{ - public class RepoRegistration - { - public RepoRegistration() - { - } - - public RepoRegistration(string enlistmentRoot, string ownerSID) - { - this.EnlistmentRoot = enlistmentRoot; - this.OwnerSID = ownerSID; - this.IsActive = true; - } - - public string EnlistmentRoot { get; set; } - public string OwnerSID { get; set; } - public bool IsActive { get; set; } - - public static RepoRegistration FromJson(string json) - { - return JsonConvert.DeserializeObject( - json, - new JsonSerializerSettings - { - MissingMemberHandling = MissingMemberHandling.Ignore - }); - } - - public override string ToString() - { - return - string.Format( - "({0} - {1}) {2}", - this.IsActive ? "Active" : "Inactive", - this.OwnerSID, - this.EnlistmentRoot); - } - - public string ToJson() - { - return JsonConvert.SerializeObject(this); - } - } -} diff --git a/Scalar.Service/RepoRegistry.cs b/Scalar.Service/RepoRegistry.cs deleted file mode 100644 index 8461f78236..0000000000 --- a/Scalar.Service/RepoRegistry.cs +++ /dev/null @@ -1,394 +0,0 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Maintenance; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.Service -{ - public class RepoRegistry : IRepoRegistry - { - public const string RegistryName = "repo-registry"; - private const string EtwArea = nameof(RepoRegistry); - private const string RegistryTempName = "repo-registry.lock"; - private const int RegistryVersion = 2; - - private string registryParentFolderPath; - private ITracer tracer; - private PhysicalFileSystem fileSystem; - private object repoLock = new object(); - private IScalarVerbRunner scalarVerb; - - public RepoRegistry( - ITracer tracer, - PhysicalFileSystem fileSystem, - string serviceDataLocation, - IScalarVerbRunner scalarVerbRunner) - { - this.tracer = tracer; - this.fileSystem = fileSystem; - this.registryParentFolderPath = serviceDataLocation; - this.scalarVerb = scalarVerbRunner; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add("registryParentFolderPath", this.registryParentFolderPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, "RepoRegistry created"); - this.tracer.RelatedEvent(EventLevel.Informational, "RepoRegistry_Created", metadata); - } - - public bool TryRegisterRepo(string repoRoot, string ownerSID, out string errorMessage) - { - errorMessage = null; - - try - { - lock (this.repoLock) - { - Dictionary allRepos = this.ReadRegistry(); - RepoRegistration repo; - if (allRepos.TryGetValue(repoRoot, out repo)) - { - if (!repo.IsActive) - { - repo.IsActive = true; - repo.OwnerSID = ownerSID; - this.WriteRegistry(allRepos); - } - } - else - { - allRepos[repoRoot] = new RepoRegistration(repoRoot, ownerSID); - this.WriteRegistry(allRepos); - } - } - - return true; - } - catch (Exception e) - { - errorMessage = string.Format("Error while registering repo {0}: {1}", repoRoot, e.ToString()); - } - - return false; - } - - public void TraceStatus() - { - try - { - lock (this.repoLock) - { - Dictionary allRepos = this.ReadRegistry(); - foreach (RepoRegistration repo in allRepos.Values) - { - this.tracer.RelatedInfo(repo.ToString()); - } - } - } - catch (Exception e) - { - this.tracer.RelatedError("Error while tracing repos: {0}", e.ToString()); - } - } - - public bool TryDeactivateRepo(string repoRoot, out string errorMessage) - { - errorMessage = null; - - try - { - lock (this.repoLock) - { - Dictionary allRepos = this.ReadRegistry(); - RepoRegistration repo; - if (allRepos.TryGetValue(repoRoot, out repo)) - { - if (repo.IsActive) - { - repo.IsActive = false; - this.WriteRegistry(allRepos); - } - - return true; - } - else - { - errorMessage = string.Format("Attempted to deactivate non-existent repo at '{0}'", repoRoot); - } - } - } - catch (Exception e) - { - errorMessage = string.Format("Error while deactivating repo {0}: {1}", repoRoot, e.ToString()); - } - - return false; - } - - public bool TryRemoveRepo(string repoRoot, out string errorMessage) - { - errorMessage = null; - - try - { - lock (this.repoLock) - { - Dictionary allRepos = this.ReadRegistry(); - if (allRepos.Remove(repoRoot)) - { - this.WriteRegistry(allRepos); - return true; - } - else - { - errorMessage = string.Format("Attempted to remove non-existent repo at '{0}'", repoRoot); - } - } - } - catch (Exception e) - { - errorMessage = string.Format("Error while removing repo {0}: {1}", repoRoot, e.ToString()); - } - - return false; - } - - public void RunMaintenanceTaskForRepos(MaintenanceTasks.Task task, string userId, int sessionId) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add(nameof(task), MaintenanceTasks.GetVerbTaskName(task)); - metadata.Add(nameof(userId), userId); - metadata.Add(nameof(sessionId), sessionId); - - List activeRepos = this.GetActiveReposForUser(userId); - if (activeRepos.Count == 0) - { - metadata.Add(TracingConstants.MessageKey.InfoMessage, "No active repos for user"); - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.RunMaintenanceTaskForRepos)}_NoRepos", - metadata); - } - else - { - string rootPath; - string errorMessage; - - foreach (RepoRegistration repo in activeRepos) - { - rootPath = Path.GetPathRoot(repo.EnlistmentRoot); - - metadata[nameof(repo.EnlistmentRoot)] = repo.EnlistmentRoot; - metadata[nameof(task)] = task; - metadata[nameof(rootPath)] = rootPath; - metadata.Remove(nameof(errorMessage)); - - if (!string.IsNullOrWhiteSpace(rootPath) && !this.fileSystem.DirectoryExists(rootPath)) - { - // If the volume does not exist we'll assume the drive was removed or is encrypted, - // and we'll leave the repo in the registry (but we won't run maintenance on it). - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.RunMaintenanceTaskForRepos)}_SkippedRepoWithMissingVolume", - metadata); - - continue; - } - - if (!this.fileSystem.DirectoryExists(repo.EnlistmentRoot)) - { - // The repo is no longer on disk (but its volume is present) - // Unregister the repo - if (this.TryRemoveRepo(repo.EnlistmentRoot, out errorMessage)) - { - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.RunMaintenanceTaskForRepos)}_RemovedMissingRepo", - metadata); - } - else - { - metadata[nameof(errorMessage)] = errorMessage; - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.RunMaintenanceTaskForRepos)}_FailedToRemoveRepo", - metadata); - } - - continue; - } - - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.RunMaintenanceTaskForRepos)}_CallingMaintenance", - metadata); - - this.scalarVerb.CallMaintenance(task, repo.EnlistmentRoot, sessionId); - } - } - } - - public Dictionary ReadRegistry() - { - Dictionary allRepos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - using (Stream stream = this.fileSystem.OpenFileStream( - Path.Combine(this.registryParentFolderPath, RegistryName), - FileMode.OpenOrCreate, - FileAccess.Read, - FileShare.Read, - callFlushFileBuffers: false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string versionString = reader.ReadLine(); - int version; - if (!int.TryParse(versionString, out version) || - version > RegistryVersion) - { - if (versionString != null) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("OnDiskVersion", versionString); - metadata.Add("ExpectedVersion", versionString); - this.tracer.RelatedError(metadata, "ReadRegistry: Unsupported version"); - } - - return allRepos; - } - - while (!reader.EndOfStream) - { - string entry = reader.ReadLine(); - if (entry.Length > 0) - { - try - { - RepoRegistration registration = RepoRegistration.FromJson(entry); - - string errorMessage; - string normalizedEnlistmentRootPath = registration.EnlistmentRoot; - if (this.fileSystem.TryGetNormalizedPath(registration.EnlistmentRoot, out normalizedEnlistmentRootPath, out errorMessage)) - { - if (!normalizedEnlistmentRootPath.Equals(registration.EnlistmentRoot, StringComparison.OrdinalIgnoreCase)) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot); - metadata.Add(nameof(normalizedEnlistmentRootPath), normalizedEnlistmentRootPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.ReadRegistry)}: Mapping registered enlistment root to final path"); - this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ReadRegistry)}_NormalizedPathMapping", metadata); - } - } - else - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot); - metadata.Add("NormalizedEnlistmentRootPath", normalizedEnlistmentRootPath); - metadata.Add("ErrorMessage", errorMessage); - this.tracer.RelatedWarning(metadata, $"{nameof(this.ReadRegistry)}: Failed to get normalized path name for registed enlistment root"); - } - - if (normalizedEnlistmentRootPath != null) - { - allRepos[normalizedEnlistmentRootPath] = registration; - } - } - catch (Exception e) - { - EventMetadata metadata = CreateEventMetadata(e); - metadata.Add("entry", entry); - this.tracer.RelatedError(metadata, "ReadRegistry: Failed to read entry"); - } - } - } - } - } - - return allRepos; - } - - public bool TryGetActiveRepos(out List repoList, out string errorMessage) - { - repoList = null; - errorMessage = null; - - lock (this.repoLock) - { - try - { - Dictionary repos = this.ReadRegistry(); - repoList = repos - .Values - .Where(repo => repo.IsActive) - .ToList(); - return true; - } - catch (Exception e) - { - errorMessage = string.Format("Unable to get list of active repos: {0}", e.ToString()); - return false; - } - } - } - - private static EventMetadata CreateEventMetadata(Exception e = null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - return metadata; - } - - private List GetActiveReposForUser(string ownerSID) - { - lock (this.repoLock) - { - try - { - Dictionary repos = this.ReadRegistry(); - return repos - .Values - .Where(repo => repo.IsActive) - .Where(repo => string.Equals(repo.OwnerSID, ownerSID, StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - } - catch (Exception e) - { - this.tracer.RelatedError("Unable to get list of active repos for user {0}: {1}", ownerSID, e.ToString()); - return new List(); - } - } - } - - private void WriteRegistry(Dictionary registry) - { - string tempFilePath = Path.Combine(this.registryParentFolderPath, RegistryTempName); - using (Stream stream = this.fileSystem.OpenFileStream( - tempFilePath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - callFlushFileBuffers: true)) - using (StreamWriter writer = new StreamWriter(stream)) - { - writer.WriteLine(RegistryVersion); - - foreach (RepoRegistration repo in registry.Values) - { - writer.WriteLine(repo.ToJson()); - } - - stream.Flush(); - } - - this.fileSystem.MoveAndOverwriteFile(tempFilePath, Path.Combine(this.registryParentFolderPath, RegistryName)); - } - } -} diff --git a/Scalar.Service/WindowsScalarService.cs b/Scalar.Service/WindowsScalarService.cs index 04712d4a39..5a26d2b1f3 100644 --- a/Scalar.Service/WindowsScalarService.cs +++ b/Scalar.Service/WindowsScalarService.cs @@ -1,6 +1,7 @@ using Scalar.Common; using Scalar.Common.FileSystem; using Scalar.Common.NamedPipes; +using Scalar.Common.RepoRegistry; using Scalar.Common.Tracing; using Scalar.Platform.Windows; using Scalar.Service.Handlers; @@ -24,7 +25,8 @@ public class WindowsScalarService : ServiceBase private ManualResetEvent serviceStopped; private string serviceName; private string serviceDataLocation; - private RepoRegistry repoRegistry; + private string repoRegistryLocation; + private IScalarRepoRegistry repoRegistry; private ProductUpgradeTimer productUpgradeTimer; private RequestHandler requestHandler; private MaintenanceTaskScheduler maintenanceTaskScheduler; @@ -47,15 +49,15 @@ public void Run() metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); this.tracer.RelatedEvent(EventLevel.Informational, $"ScalarService_{nameof(this.Run)}", metadata); - this.repoRegistry = new RepoRegistry( + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + this.repoRegistry = new ScalarRepoRegistry( this.tracer, - new PhysicalFileSystem(), - this.serviceDataLocation, - new WindowsScalarVerbRunner(this.tracer)); + fileSystem, + this.repoRegistryLocation); - this.maintenanceTaskScheduler = new MaintenanceTaskScheduler(this.tracer, this.repoRegistry); + this.maintenanceTaskScheduler = new MaintenanceTaskScheduler(this.tracer, fileSystem, new WindowsScalarVerbRunner(this.tracer), this.repoRegistry); - this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); + this.requestHandler = new RequestHandler(this.tracer, EtwArea); string pipeName = ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.serviceName); this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); @@ -138,13 +140,10 @@ protected override void OnSessionChange(SessionChangeDescription changeDescripti this.LaunchServiceUIIfNotRunning(changeDescription.SessionId); - using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational)) - { - this.maintenanceTaskScheduler.RegisterUser( - new UserAndSession( - ScalarPlatform.Instance.GetUserIdFromLoginSessionId(changeDescription.SessionId, this.tracer), - changeDescription.SessionId)); - } + this.maintenanceTaskScheduler.RegisterUser( + new UserAndSession( + ScalarPlatform.Instance.GetUserIdFromLoginSessionId(changeDescription.SessionId, this.tracer), + changeDescription.SessionId)); } else if (changeDescription.Reason == SessionChangeReason.SessionLogoff) { @@ -187,6 +186,7 @@ protected override void OnStart(string[] args) try { this.serviceDataLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(this.serviceName); + this.repoRegistryLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.RepoRegistry.RegistryDirectoryName); this.CreateAndConfigureProgramDataDirectories(); this.Start(); } @@ -257,11 +257,12 @@ private void CreateServiceLogsDirectory(string serviceLogsDirectoryPath) private void CreateAndConfigureProgramDataDirectories() { - string serviceDataRootPath = Path.GetDirectoryName(this.serviceDataLocation); + string serviceDataRootPath = ScalarPlatform.Instance.GetDataRootForScalar(); DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceDataRootPath); // Create Scalar.Service and Scalar.Upgrade related directories (if they don't already exist) + // TODO #136: Determine if we still should be creating Scalar.Service here DirectoryEx.CreateDirectory(serviceDataRootPath, serviceDataRootSecurity); DirectoryEx.CreateDirectory(this.serviceDataLocation, serviceDataRootSecurity); DirectoryEx.CreateDirectory(ProductUpgraderInfo.GetUpgradeProtectedDataDirectory(), serviceDataRootSecurity); @@ -269,12 +270,13 @@ private void CreateAndConfigureProgramDataDirectories() // Ensure the ACLs are set correctly on any files or directories that were already created (e.g. after upgrading Scalar) DirectoryEx.SetAccessControl(serviceDataRootPath, serviceDataRootSecurity); - // Special rules for the upgrader logs, as non-elevated users need to be be able to write - this.CreateAndConfigureLogDirectory(ProductUpgraderInfo.GetLogDirectoryPath()); - this.CreateAndConfigureLogDirectory(ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.UIName)); + // Special rules for the upgrader logs and registry, as non-elevated users need to be be able to write + this.CreateAndConfigureUserWriteableDirectory(this.repoRegistryLocation); + this.CreateAndConfigureUserWriteableDirectory(ProductUpgraderInfo.GetLogDirectoryPath()); + this.CreateAndConfigureUserWriteableDirectory(ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.UIName)); } - private void CreateAndConfigureLogDirectory(string path) + private void CreateAndConfigureUserWriteableDirectory(string path) { string upgradeLogsPath = ProductUpgraderInfo.GetLogDirectoryPath(); @@ -287,7 +289,7 @@ private void CreateAndConfigureLogDirectory(string path) metadata.Add(nameof(error), error); this.tracer.RelatedWarning( metadata, - $"{nameof(this.CreateAndConfigureLogDirectory)}: Failed to create upgrade logs directory", + $"{nameof(this.CreateAndConfigureUserWriteableDirectory)}: Failed to create upgrade logs directory", Keywords.Telemetry); } } diff --git a/Scalar.TestInfrastructure/Should/EnumerableShouldExtensions.cs b/Scalar.TestInfrastructure/Should/EnumerableShouldExtensions.cs index c3dca5f90e..281deaf6b7 100644 --- a/Scalar.TestInfrastructure/Should/EnumerableShouldExtensions.cs +++ b/Scalar.TestInfrastructure/Should/EnumerableShouldExtensions.cs @@ -89,6 +89,36 @@ public static IEnumerable ShouldMatchInOrder(this IEnumerable group, pa } public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues, Func equals, string message = "") + { + return group.ShouldMatch(expectedValues, equals, shouldMatchInOrder: true, message: message); + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params T[] expectedValues) + { + return group.ShouldMatchInOrder((IEnumerable)expectedValues); + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues) + { + return group.ShouldMatchInOrder(expectedValues, (t1, t2) => t1.Equals(t2)); + } + + public static IEnumerable ShouldMatch(this IEnumerable group, IEnumerable expectedValues, Func equals, string message = "") + { + return group.ShouldMatch(expectedValues, equals, shouldMatchInOrder: false, message: message); + } + + public static IEnumerable ShouldMatch(this IEnumerable group, params T[] expectedValues) + { + return group.ShouldMatch((IEnumerable)expectedValues); + } + + public static IEnumerable ShouldMatch(this IEnumerable group, IEnumerable expectedValues) + { + return group.ShouldMatch(expectedValues, (t1, t2) => t1.Equals(t2)); + } + + private static IEnumerable ShouldMatch(this IEnumerable group, IEnumerable expectedValues, Func equals, bool shouldMatchInOrder, string message = "") { List groupList = new List(group); List expectedValuesList = new List(expectedValues); @@ -114,6 +144,17 @@ public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IE errorMessage.AppendLine(string.Format("Missing: {0}", groupMissingItem)); } + if (shouldMatchInOrder) + { + for (int i = 0; i < groupList.Count; ++i) + { + if (!equals(groupList[i], expectedValuesList[i])) + { + errorMessage.AppendLine($"Items ordered differently, found: {groupList[i]} expected: {expectedValuesList[i]}"); + } + } + } + if (errorMessage.Length > 0) { Assert.Fail("{0}\r\n{1}", message, errorMessage); @@ -122,16 +163,6 @@ public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IE return group; } - public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params T[] expectedValues) - { - return group.ShouldMatchInOrder((IEnumerable)expectedValues); - } - - public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues) - { - return group.ShouldMatchInOrder(expectedValues, (t1, t2) => t1.Equals(t2)); - } - private class Comparer : IEqualityComparer { private Func equals; @@ -148,7 +179,10 @@ public bool Equals(T x, T y) public int GetHashCode(T obj) { - return obj.GetHashCode(); + // Return a constant to force Equals(...) to be called. + // This is required for custom T types that do not implement + // GetHashCode + return 1; } } } diff --git a/Scalar.TestInfrastructure/Should/StringShouldExtensions.cs b/Scalar.TestInfrastructure/Should/StringShouldExtensions.cs index 8c7eb6b761..603aecaeff 100644 --- a/Scalar.TestInfrastructure/Should/StringShouldExtensions.cs +++ b/Scalar.TestInfrastructure/Should/StringShouldExtensions.cs @@ -64,5 +64,11 @@ public static string ShouldContainOneOf(this string actualValue, params string[] Assert.Fail("No expected substrings found in '{0}'", actualValue); return actualValue; } + + public static string ShouldNotBeNullOrEmpty(this string value) + { + Assert.IsFalse(string.IsNullOrEmpty(value)); + return value; + } } } diff --git a/Scalar.UnitTests/Common/RepoRegistry/ScalarRepoRegistryTests.cs b/Scalar.UnitTests/Common/RepoRegistry/ScalarRepoRegistryTests.cs new file mode 100644 index 0000000000..8e1e961584 --- /dev/null +++ b/Scalar.UnitTests/Common/RepoRegistry/ScalarRepoRegistryTests.cs @@ -0,0 +1,253 @@ +using Moq; +using NUnit.Framework; +using Scalar.Common.FileSystem; +using Scalar.Common.RepoRegistry; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.UnitTests.Common.RepoRegistry +{ + [TestFixture] + public class ScalarRepoRegistryTests + { + private readonly string registryFolderPath = Path.Combine(MockFileSystem.GetMockRoot(), "Scalar", "UnitTests.RepoRegistry"); + + [TestCase] + public void TryRegisterRepo_CreatesMissingRegistryDirectory() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + + List registrations = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), "testUser") + }; + + fileSystem.DirectoryExists(this.registryFolderPath).ShouldBeFalse(); + this.RegisterRepos(registry, registrations); + fileSystem.DirectoryExists(this.registryFolderPath).ShouldBeTrue("Registering a repo should have created the missing registry directory"); + + this.RegistryShouldContainRegistrations(registry, registrations); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryRegisterRepo_FailsIfMissingRegistryDirectoryCantBeCreated() + { + Mock mockFileSystem = new Mock(MockBehavior.Strict); + mockFileSystem.Setup(fileSystem => fileSystem.DirectoryExists(this.registryFolderPath)).Returns(false); + mockFileSystem.Setup(fileSystem => fileSystem.CreateDirectory(this.registryFolderPath)).Throws(new UnauthorizedAccessException()); + + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + mockFileSystem.Object, + this.registryFolderPath); + + string testRepoRoot = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"); + string testUserId = "testUser"; + + registry.TryRegisterRepo(testRepoRoot, testUserId, out string errorMessage).ShouldBeFalse(); + errorMessage.ShouldNotBeNullOrEmpty(); + + mockFileSystem.VerifyAll(); + } + + [TestCase] + public void TryRegisterRepo_RegisterMultipleRepos() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + + List repoRegistrations = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo2"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo1"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo2"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos2", "Repo1"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo1"), "ThirdUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo2"), "ThirdUser") + }; + + this.RegisterRepos(registry, repoRegistrations); + this.RegistryShouldContainRegistrations(registry, repoRegistrations); + } + + [TestCase] + public void TryRegisterRepo_UpdatesUsersForExistingRegistrations() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + + List registrationsPart1 = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo1"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos2", "Repo1"), "testUser") + }; + + List registrationsPart2 = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo2"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo2"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo1"), "ThirdUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo2"), "ThirdUser") + }; + + this.RegisterRepos(registry, registrationsPart1.Concat(registrationsPart2)); + this.RegistryShouldContainRegistrations(registry, registrationsPart1.Concat(registrationsPart2)); + + // Update the users on some registrations + foreach (ScalarRepoRegistration registration in registrationsPart2) + { + registration.UserId = $"UPDATED_{registration.UserId}"; + } + + // Just register the updates + this.RegisterRepos(registry, registrationsPart2); + + // The unchanged + updated entries should be present + this.RegistryShouldContainRegistrations(registry, registrationsPart1.Concat(registrationsPart2)); + } + + [TestCase] + public void TryUnregisterRepo_RemovesRegisteredRepos() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + + List registrationsPart1 = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo1"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos2", "Repo1"), "testUser") + }; + + List registrationsPart2 = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo2"), "testUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "MoreRepos", "Repo2"), "user2"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo1"), "ThirdUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos3", "Repo2"), "ThirdUser") + }; + + this.RegisterRepos(registry, registrationsPart1.Concat(registrationsPart2)); + this.RegistryShouldContainRegistrations(registry, registrationsPart1.Concat(registrationsPart2)); + this.UnregisterRepos(registry, registrationsPart2); + this.RegistryShouldContainRegistrations(registry, registrationsPart1); + } + + [TestCase] + public void TryUnregisterRepo_FailsIfRegistryDirectoryMissing() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + fileSystem.DirectoryExists(this.registryFolderPath).ShouldBeFalse(); + registry.TryUnregisterRepo(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), out string errorMessage).ShouldBeFalse(); + errorMessage.ShouldNotBeNullOrEmpty(); + fileSystem.DirectoryExists(this.registryFolderPath).ShouldBeFalse(); + } + + [TestCase] + public void TryUnregisterRepo_FailsForUnregisteredRepo() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.GetDirectoryName(this.registryFolderPath), null, null)); + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + fileSystem, + this.registryFolderPath); + + List registrations = new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"), "testUser") + }; + + this.RegisterRepos(registry, registrations); + registry.TryUnregisterRepo(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo2"), out string errorMessage).ShouldBeFalse(); + errorMessage.ShouldNotBeNullOrEmpty(); + this.RegistryShouldContainRegistrations(registry, registrations); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryUnregisterRepo_FailsIfDeleteFails() + { + string repoPath = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "Repo1"); + string registrationFilePath = Path.Combine(this.registryFolderPath, ScalarRepoRegistry.GetRepoRootSha(repoPath) + ".repo"); + + Mock mockFileSystem = new Mock(MockBehavior.Strict); + mockFileSystem.Setup(fileSystem => fileSystem.FileExists(registrationFilePath)).Returns(true); + mockFileSystem.Setup(fileSystem => fileSystem.DeleteFile(registrationFilePath)).Throws(new UnauthorizedAccessException()); + + ScalarRepoRegistry registry = new ScalarRepoRegistry( + new MockTracer(), + mockFileSystem.Object, + this.registryFolderPath); + + registry.TryUnregisterRepo(repoPath, out string errorMessage).ShouldBeFalse(); + errorMessage.ShouldNotBeNullOrEmpty(); + + mockFileSystem.VerifyAll(); + } + + [TestCase] + public void GetRepoRootSha_IsStable() + { + // Don't use MockFileSystem.GetMockRoot() as the SHA is tied to the specific string + // passed to GetRepoRootSha + ScalarRepoRegistry.GetRepoRootSha(@"B:\Repos\Repo1").ShouldEqual("f42a90ef8218f011c5dbcb642bd8eb6c08add452"); + ScalarRepoRegistry.GetRepoRootSha(@"B:\folder\repoRoot").ShouldEqual("5a3c2a461d342525a532b03479e6cdeb775fa497"); + } + + private static bool RepoRegistrationsEqual(ScalarRepoRegistration repo1, ScalarRepoRegistration repo2) + { + return + repo1.NormalizedRepoRoot.Equals(repo2.NormalizedRepoRoot, StringComparison.Ordinal) && + repo1.UserId.Equals(repo2.UserId, StringComparison.Ordinal); + } + + private void RegisterRepos(ScalarRepoRegistry registry, IEnumerable registrations) + { + foreach (ScalarRepoRegistration registration in registrations) + { + registry.TryRegisterRepo(registration.NormalizedRepoRoot, registration.UserId, out string errorMessage).ShouldBeTrue(); + errorMessage.ShouldBeNull(); + } + } + + private void UnregisterRepos(ScalarRepoRegistry registry, IEnumerable registrations) + { + foreach (ScalarRepoRegistration registration in registrations) + { + registry.TryUnregisterRepo(registration.NormalizedRepoRoot, out string errorMessage).ShouldBeTrue(); + errorMessage.ShouldBeNull(); + } + } + + private void RegistryShouldContainRegistrations(ScalarRepoRegistry registry, IEnumerable registrations) + { + registry.GetRegisteredRepos().ShouldMatch(registrations, RepoRegistrationsEqual); + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockTracer.cs b/Scalar.UnitTests/Mock/Common/MockTracer.cs index fbc525714e..c1a70e6a7b 100644 --- a/Scalar.UnitTests/Mock/Common/MockTracer.cs +++ b/Scalar.UnitTests/Mock/Common/MockTracer.cs @@ -13,6 +13,7 @@ public class MockTracer : ITracer public MockTracer() { this.waitEvent = new AutoResetEvent(false); + this.RelatedEvents = new List(); this.RelatedInfoEvents = new List(); this.RelatedWarningEvents = new List(); this.RelatedErrorEvents = new List(); @@ -21,6 +22,7 @@ public MockTracer() public MockTracer StartActivityTracer { get; private set; } public string WaitRelatedEventName { get; set; } + public List RelatedEvents { get; } public List RelatedInfoEvents { get; } public List RelatedWarningEvents { get; } public List RelatedErrorEvents { get; } @@ -32,10 +34,7 @@ public void WaitForRelatedEvent() public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata) { - if (eventName == this.WaitRelatedEventName) - { - this.waitEvent.Set(); - } + this.RelatedEvent(error, eventName, metadata, Keywords.None); } public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata, Keywords keyword) @@ -44,6 +43,8 @@ public void RelatedEvent(EventLevel error, string eventName, EventMetadata metad { this.waitEvent.Set(); } + + this.RelatedEvents.Add($"EventName:'{eventName}', metadata: {JsonConvert.SerializeObject(metadata)}"); } public void RelatedInfo(string message) diff --git a/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs b/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs index 8096c02558..51850bb4c0 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs @@ -1,3 +1,4 @@ +using NUnit.Framework; using Scalar.Common; using Scalar.Common.FileSystem; using Scalar.Common.Tracing; @@ -39,6 +40,32 @@ public MockFileSystem(MockDirectory rootDirectory) /// public bool DeleteNonExistentFileThrowsException { get; set; } + /// + /// Returns the root of the mock filesystem + /// + /// + /// The paths returned are highly unlikely to be used on a real machine, + /// making it easier to catch product code that is (incorrectly) + /// interacting directly with the file system via System.IO or PInvoke. + /// + public static string GetMockRoot() + { + switch (Path.DirectorySeparatorChar) + { + case '/': + return "/scalar_ut"; + + case '\\': + // Use a single letter (rather than something like "mock:") so + // that helper methods like Path.GetPathRoot work correctly + return "B:"; // Second floppy drive + + default: + Assert.Fail($"Unknown DirectorySeparatorChar '{Path.DirectorySeparatorChar}'"); + return null; + } + } + public override void DeleteDirectory(string path, bool recursive = true) { if (!recursive) @@ -284,6 +311,51 @@ public override IEnumerable EnumerateDirectories(string path) } } + public override IEnumerable EnumerateFiles(string path, string searchPattern) + { + string searchSuffix = null; + bool matchAll = string.IsNullOrEmpty(searchPattern) || searchPattern == "*"; + + if (!matchAll) + { + // Only support matching "*" + if (!searchPattern.StartsWith("*", StringComparison.Ordinal)) + { + throw new NotImplementedException("Unsupported search pattern"); + } + + if (searchPattern.IndexOf('*', startIndex: 1) != -1) + { + throw new NotImplementedException("Unsupported search pattern"); + } + + if (searchPattern.Contains("?")) + { + throw new NotImplementedException("Unsupported search pattern"); + } + + searchSuffix = searchPattern.Substring(1); + } + + MockDirectory directory = this.RootDirectory.FindDirectory(path); + directory.ShouldNotBeNull(); + + if (directory != null) + { + foreach (MockFile file in directory.Files.Values) + { + if (matchAll) + { + yield return file.FullName; + } + else if (file.Name.EndsWith(searchSuffix, StringComparison.OrdinalIgnoreCase)) + { + yield return file.FullName; + } + } + } + } + public override FileProperties GetFileProperties(string path) { MockFile file = this.RootDirectory.FindFile(path); diff --git a/Scalar.UnitTests/Service/Mac/MacScalarVerbRunnerTests.cs b/Scalar.UnitTests/Service/Mac/MacScalarVerbRunnerTests.cs new file mode 100644 index 0000000000..fc100a57cf --- /dev/null +++ b/Scalar.UnitTests/Service/Mac/MacScalarVerbRunnerTests.cs @@ -0,0 +1,52 @@ +using Moq; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Maintenance; +using Scalar.Service; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using System.IO; + +namespace Scalar.UnitTests.Service.Mac +{ + [TestFixture] + public class MacScalarVerbRunnerTests + { + private const int ExpectedActiveUserId = 502; + private static readonly string ExpectedActiveRepoPath = Path.Combine(MockFileSystem.GetMockRoot(), "code", "repo2"); + + private MockTracer tracer; + private MockPlatform scalarPlatform; + + [SetUp] + public void SetUp() + { + this.tracer = new MockTracer(); + this.scalarPlatform = (MockPlatform)ScalarPlatform.Instance; + this.scalarPlatform.MockCurrentUser = ExpectedActiveUserId.ToString(); + } + + [TestCase] + public void CallMaintenance_LaunchesVerbUsingCorrectArgs() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.FetchCommitsAndTrees; + string taskVerbName = MaintenanceTasks.GetVerbTaskName(task); + string executable = @"/bin/launchctl"; + string scalarBinPath = Path.Combine(this.scalarPlatform.Constants.ScalarBinDirectoryPath, this.scalarPlatform.Constants.ScalarExecutableName); + string expectedArgs = + $"asuser {ExpectedActiveUserId} {scalarBinPath} maintenance \"{ExpectedActiveRepoPath}\" --{ScalarConstants.VerbParameters.Maintenance.Task} {taskVerbName} --{ScalarConstants.VerbParameters.InternalUseOnly} {new InternalVerbParameters(startedByService: true).ToJson()}"; + + Mock mountLauncherMock = new Mock(MockBehavior.Strict, this.tracer); + mountLauncherMock.Setup(mp => mp.LaunchProcess( + executable, + expectedArgs, + ExpectedActiveRepoPath)) + .Returns(new ProcessResult(output: string.Empty, errors: string.Empty, exitCode: 0)); + + MacScalarVerbRunner verbProcess = new MacScalarVerbRunner(this.tracer, mountLauncherMock.Object); + verbProcess.CallMaintenance(task, ExpectedActiveRepoPath, ExpectedActiveUserId); + + mountLauncherMock.VerifyAll(); + } + } +} diff --git a/Scalar.UnitTests/Service/Mac/MacServiceTests.cs b/Scalar.UnitTests/Service/Mac/MacServiceTests.cs deleted file mode 100644 index 123c0ad617..0000000000 --- a/Scalar.UnitTests/Service/Mac/MacServiceTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Maintenance; -using Scalar.Service; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using System.IO; - -namespace Scalar.UnitTests.Service.Mac -{ - [TestFixture] - public class MacServiceTests - { - private const int ExpectedActiveUserId = 502; - private const int ExpectedSessionId = 502; - private static readonly string ExpectedActiveRepoPath = Path.Combine("mock:", "code", "repo2"); - private static readonly string ServiceDataLocation = Path.Combine("mock:", "registryDataFolder"); - - private MockFileSystem fileSystem; - private MockTracer tracer; - private MockPlatform scalarPlatform; - - [SetUp] - public void SetUp() - { - this.tracer = new MockTracer(); - this.fileSystem = new MockFileSystem(new MockDirectory(ServiceDataLocation, null, null)); - this.scalarPlatform = (MockPlatform)ScalarPlatform.Instance; - this.scalarPlatform.MockCurrentUser = ExpectedActiveUserId.ToString(); - } - - [TestCase] - public void RepoRegistryRemovesRegisteredRepoIfMissingFromDisk() - { - Mock repoMounterMock = new Mock(MockBehavior.Strict); - - this.fileSystem.DirectoryExists(ExpectedActiveRepoPath).ShouldBeFalse($"{ExpectedActiveRepoPath} should not exist"); - - MaintenanceTasks.Task task = MaintenanceTasks.Task.FetchCommitsAndTrees; - - this.CreateTestRepos(ServiceDataLocation); - RepoRegistry repoRegistry = new RepoRegistry( - this.tracer, - this.fileSystem, - ServiceDataLocation, - repoMounterMock.Object); - - repoRegistry.RunMaintenanceTaskForRepos(task, ExpectedActiveUserId.ToString(), ExpectedSessionId); - repoMounterMock.VerifyAll(); - - repoRegistry.ReadRegistry().ShouldNotContain(entry => entry.Key.Equals(ExpectedActiveRepoPath)); - } - - [TestCase] - public void RepoRegistryCallsMaintenanceVerbOnlyForRegisteredRepos() - { - Mock repoMounterMock = new Mock(MockBehavior.Strict); - - this.fileSystem.CreateDirectory(ExpectedActiveRepoPath); - - MaintenanceTasks.Task task = MaintenanceTasks.Task.FetchCommitsAndTrees; - repoMounterMock.Setup(mp => mp.CallMaintenance(task, ExpectedActiveRepoPath, ExpectedActiveUserId)).Returns(true); - - this.CreateTestRepos(ServiceDataLocation); - - RepoRegistry repoRegistry = new RepoRegistry( - this.tracer, - this.fileSystem, - ServiceDataLocation, - repoMounterMock.Object); - - repoRegistry.RunMaintenanceTaskForRepos(task, ExpectedActiveUserId.ToString(), ExpectedSessionId); - - repoMounterMock.VerifyAll(); - } - - [TestCase] - public void MaintenanceVerbLaunchedUsingCorrectArgs() - { - MaintenanceTasks.Task task = MaintenanceTasks.Task.FetchCommitsAndTrees; - string taskVerbName = MaintenanceTasks.GetVerbTaskName(task); - string executable = @"/bin/launchctl"; - string scalarBinPath = Path.Combine(this.scalarPlatform.Constants.ScalarBinDirectoryPath, this.scalarPlatform.Constants.ScalarExecutableName); - string expectedArgs = - $"asuser {ExpectedActiveUserId} {scalarBinPath} maintenance \"{ExpectedActiveRepoPath}\" --{ScalarConstants.VerbParameters.Maintenance.Task} {taskVerbName} --{ScalarConstants.VerbParameters.InternalUseOnly} {new InternalVerbParameters(startedByService: true).ToJson()}"; - - Mock mountLauncherMock = new Mock(MockBehavior.Strict, this.tracer); - mountLauncherMock.Setup(mp => mp.LaunchProcess( - executable, - expectedArgs, - ExpectedActiveRepoPath)) - .Returns(new ProcessResult(output: string.Empty, errors: string.Empty, exitCode: 0)); - - MacScalarVerbRunner verbProcess = new MacScalarVerbRunner(this.tracer, mountLauncherMock.Object); - verbProcess.CallMaintenance(task, ExpectedActiveRepoPath, ExpectedActiveUserId); - - mountLauncherMock.VerifyAll(); - } - - private void CreateTestRepos(string dataLocation) - { - string repo1 = Path.Combine("mock:", "code", "repo1"); - string repo2 = ExpectedActiveRepoPath; - string repo3 = Path.Combine("mock:", "code", "repo3"); - string repo4 = Path.Combine("mock:", "code", "repo4"); - - this.fileSystem.WriteAllText( - Path.Combine(dataLocation, RepoRegistry.RegistryName), - $@"1 - {{""EnlistmentRoot"":""{repo1.Replace("\\", "\\\\")}"",""OwnerSID"":502,""IsActive"":false}} - {{""EnlistmentRoot"":""{repo2.Replace("\\", "\\\\")}"",""OwnerSID"":502,""IsActive"":true}} - {{""EnlistmentRoot"":""{repo3.Replace("\\", "\\\\")}"",""OwnerSID"":501,""IsActive"":false}} - {{""EnlistmentRoot"":""{repo4.Replace("\\", "\\\\")}"",""OwnerSID"":501,""IsActive"":true}} - "); - } - } -} diff --git a/Scalar.UnitTests/Service/MaintenanceTaskSchedulerTests.cs b/Scalar.UnitTests/Service/MaintenanceTaskSchedulerTests.cs new file mode 100644 index 0000000000..52dd635e75 --- /dev/null +++ b/Scalar.UnitTests/Service/MaintenanceTaskSchedulerTests.cs @@ -0,0 +1,219 @@ +using Moq; +using NUnit.Framework; +using Scalar.Common.FileSystem; +using Scalar.Common.Maintenance; +using Scalar.Common.RepoRegistry; +using Scalar.Service; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Service +{ + [TestFixture] + public class MaintenanceTaskSchedulerTests + { + private MockTracer mockTracer; + private Mock mockFileSystem; + private Mock mockVerbRunner; + private Mock mockRepoRegistry; + private Mock mockRegisteredUserStore; + + [SetUp] + public void Setup() + { + this.mockTracer = new MockTracer(); + this.mockFileSystem = new Mock(MockBehavior.Strict); + this.mockVerbRunner = new Mock(MockBehavior.Strict); + this.mockRepoRegistry = new Mock(MockBehavior.Strict); + this.mockRegisteredUserStore = new Mock(MockBehavior.Strict); + } + + [TearDown] + public void TearDown() + { + this.mockFileSystem.VerifyAll(); + this.mockVerbRunner.VerifyAll(); + this.mockRepoRegistry.VerifyAll(); + this.mockRegisteredUserStore.VerifyAll(); + } + + [TestCase] + public void RegisterUser() + { + using (MaintenanceTaskScheduler taskScheduler = new MaintenanceTaskScheduler( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object)) + { + taskScheduler.RegisteredUser.ShouldBeNull(); + + UserAndSession testUser1 = new UserAndSession("testUser1", sessionId: 1); + UserAndSession testUser2 = new UserAndSession("testUser2", sessionId: 2); + + taskScheduler.RegisterUser(testUser1); + taskScheduler.RegisteredUser.UserId.ShouldEqual(testUser1.UserId); + taskScheduler.RegisteredUser.SessionId.ShouldEqual(testUser1.SessionId); + + taskScheduler.RegisterUser(testUser2); + taskScheduler.RegisteredUser.UserId.ShouldEqual(testUser2.UserId); + taskScheduler.RegisteredUser.SessionId.ShouldEqual(testUser2.SessionId); + } + } + + [TestCase] + public void MaintenanceTask_Execute_NoRegisteredUser() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.PackFiles; + + this.mockRegisteredUserStore.SetupGet(mrus => mrus.RegisteredUser).Returns((UserAndSession)null); + + MaintenanceTaskScheduler.MaintenanceTask maintenanceTask = new MaintenanceTaskScheduler.MaintenanceTask( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object, + this.mockRegisteredUserStore.Object, + task); + + maintenanceTask.Execute(); + this.mockTracer.RelatedInfoEvents.ShouldContain(entry => entry.Contains($"Skipping '{task}', no registered user")); + } + + [TestCase] + public void MaintenanceTask_Execute_SkipsReposThatDoNotMatchRegisteredUser() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.PackFiles; + + UserAndSession testUser = new UserAndSession("testUserId", sessionId: 1); + this.mockRegisteredUserStore.SetupGet(mrus => mrus.RegisteredUser).Returns(testUser); + + this.mockRepoRegistry.Setup(reg => reg.GetRegisteredRepos()).Returns( + new List + { + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot"), "nonMatchingUser"), + new ScalarRepoRegistration(Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot2"), "nonMatchingUser2") + }); + + MaintenanceTaskScheduler.MaintenanceTask maintenanceTask = new MaintenanceTaskScheduler.MaintenanceTask( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object, + this.mockRegisteredUserStore.Object, + task); + + maintenanceTask.Execute(); + this.mockTracer.RelatedEvents.ShouldContain(entry => entry.Contains("\"reposInRegistryForUser\":0")); + } + + [TestCase] + public void MaintenanceTask_Execute_SkipsRegisteredRepoIfVolumeDoesNotExist() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.PackFiles; + + UserAndSession testUser = new UserAndSession("testUserId", sessionId: 1); + this.mockRegisteredUserStore.SetupGet(mrus => mrus.RegisteredUser).Returns(testUser); + + string repoPath = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot"); + this.mockRepoRegistry.Setup(reg => reg.GetRegisteredRepos()).Returns( + new List + { + new ScalarRepoRegistration(repoPath, testUser.UserId) + }); + + this.mockFileSystem.Setup(fs => fs.DirectoryExists(Path.GetPathRoot(repoPath))).Returns(false); + + MaintenanceTaskScheduler.MaintenanceTask maintenanceTask = new MaintenanceTaskScheduler.MaintenanceTask( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object, + this.mockRegisteredUserStore.Object, + task); + + maintenanceTask.Execute(); + this.mockTracer.RelatedEvents.ShouldContain(entry => entry.Contains("SkippedRepoWithMissingVolume")); + } + + [TestCase] + public void MaintenanceTask_Execute_UnregistersRepoIfMissing() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.PackFiles; + + UserAndSession testUser = new UserAndSession("testUserId", sessionId: 1); + this.mockRegisteredUserStore.SetupGet(mrus => mrus.RegisteredUser).Returns(testUser); + + string repoPath = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot"); + this.mockRepoRegistry.Setup(reg => reg.GetRegisteredRepos()).Returns( + new List + { + new ScalarRepoRegistration(repoPath, testUser.UserId) + }); + + // Validate that TryUnregisterRepo will be called for repoPath + string emptyString = string.Empty; + this.mockRepoRegistry.Setup(reg => reg.TryUnregisterRepo(repoPath, out emptyString)).Returns(true); + + // The root volume should exist + this.mockFileSystem.Setup(fs => fs.DirectoryExists(Path.GetPathRoot(repoPath))).Returns(true); + + // The repo itself does not exist + this.mockFileSystem.Setup(fs => fs.DirectoryExists(repoPath)).Returns(false); + + MaintenanceTaskScheduler.MaintenanceTask maintenanceTask = new MaintenanceTaskScheduler.MaintenanceTask( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object, + this.mockRegisteredUserStore.Object, + task); + + maintenanceTask.Execute(); + this.mockTracer.RelatedEvents.ShouldContain(entry => entry.Contains("RemovedMissingRepo")); + } + + [TestCase] + public void MaintenanceTask_Execute_CallsMaintenanceVerbOnlyForRegisteredRepos() + { + MaintenanceTasks.Task task = MaintenanceTasks.Task.PackFiles; + + UserAndSession testUser = new UserAndSession("testUserId", sessionId: 1); + UserAndSession secondUser = new UserAndSession("testUserId2", sessionId: 1); + string repoPath1 = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot"); + string repoPath2 = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "repoRoot2"); + string secondUsersRepoPath = Path.Combine(MockFileSystem.GetMockRoot(), "Repos", "secondUsersRepo"); + + this.mockRegisteredUserStore.SetupGet(mrus => mrus.RegisteredUser).Returns(testUser); + + this.mockRepoRegistry.Setup(reg => reg.GetRegisteredRepos()).Returns( + new List + { + new ScalarRepoRegistration(repoPath1, testUser.UserId), + new ScalarRepoRegistration(secondUsersRepoPath, secondUser.UserId), + new ScalarRepoRegistration(repoPath2, testUser.UserId) + }); + + // The root volume and repos exist + this.mockFileSystem.Setup(fs => fs.DirectoryExists(Path.GetPathRoot(repoPath1))).Returns(true); + this.mockFileSystem.Setup(fs => fs.DirectoryExists(repoPath1)).Returns(true); + this.mockFileSystem.Setup(fs => fs.DirectoryExists(repoPath2)).Returns(true); + + this.mockVerbRunner.Setup(vr => vr.CallMaintenance(task, repoPath1, testUser.SessionId)).Returns(true); + this.mockVerbRunner.Setup(vr => vr.CallMaintenance(task, repoPath2, testUser.SessionId)).Returns(true); + + MaintenanceTaskScheduler.MaintenanceTask maintenanceTask = new MaintenanceTaskScheduler.MaintenanceTask( + this.mockTracer, + this.mockFileSystem.Object, + this.mockVerbRunner.Object, + this.mockRepoRegistry.Object, + this.mockRegisteredUserStore.Object, + task); + + maintenanceTask.Execute(); + } + } +} diff --git a/Scalar.UnitTests/Service/RepoRegistryTests.cs b/Scalar.UnitTests/Service/RepoRegistryTests.cs deleted file mode 100644 index eaecda4040..0000000000 --- a/Scalar.UnitTests/Service/RepoRegistryTests.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Moq; -using NUnit.Framework; -using Scalar.Service; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.UnitTests.Service -{ - [TestFixture] - public class RepoRegistryTests - { - private Mock mockRepoMounter; - - [SetUp] - public void Setup() - { - this.mockRepoMounter = new Mock(MockBehavior.Strict); - } - - [TearDown] - public void TearDown() - { - this.mockRepoMounter.VerifyAll(); - } - - [TestCase] - public void TryRegisterRepo_EmptyRegistry() - { - string dataLocation = Path.Combine("mock:", "registryDataFolder"); - - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); - RepoRegistry registry = new RepoRegistry( - new MockTracer(), - fileSystem, - dataLocation, - this.mockRepoMounter.Object); - - string repoRoot = Path.Combine("c:", "test"); - string ownerSID = Guid.NewGuid().ToString(); - - string errorMessage; - registry.TryRegisterRepo(repoRoot, ownerSID, out errorMessage).ShouldEqual(true); - - Dictionary verifiableRegistry = registry.ReadRegistry(); - verifiableRegistry.Count.ShouldEqual(1); - this.VerifyRepo(verifiableRegistry[repoRoot], ownerSID, expectedIsActive: true); - } - - [TestCase] - public void TryGetActiveRepos_BeforeAndAfterActivateAndDeactivate() - { - string dataLocation = Path.Combine("mock:", "registryDataFolder"); - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); - RepoRegistry registry = new RepoRegistry( - new MockTracer(), - fileSystem, - dataLocation, - this.mockRepoMounter.Object); - - string repo1Root = Path.Combine("mock:", "test", "repo1"); - string owner1SID = Guid.NewGuid().ToString(); - string repo2Root = Path.Combine("mock:", "test", "repo2"); - string owner2SID = Guid.NewGuid().ToString(); - string repo3Root = Path.Combine("mock:", "test", "repo3"); - string owner3SID = Guid.NewGuid().ToString(); - - // Register all 3 repos - string errorMessage; - registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true); - registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true); - registry.TryRegisterRepo(repo3Root, owner3SID, out errorMessage).ShouldEqual(true); - - // Confirm all 3 active - List activeRepos; - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(3); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo2Root)), owner2SID, expectedIsActive: true); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true); - - // Deactive repo 2 - registry.TryDeactivateRepo(repo2Root, out errorMessage).ShouldEqual(true); - - // Confirm repos 1 and 3 still active - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(2); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true); - - // Activate repo 2 - registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true); - - // Confirm all 3 active - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(3); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo2Root)), owner2SID, expectedIsActive: true); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo3Root)), owner3SID, expectedIsActive: true); - } - - [TestCase] - public void TryDeactivateRepo() - { - string dataLocation = Path.Combine("mock:", "registryDataFolder"); - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); - RepoRegistry registry = new RepoRegistry( - new MockTracer(), - fileSystem, - dataLocation, - this.mockRepoMounter.Object); - - string repo1Root = Path.Combine("mock:", "test", "repo1"); - string owner1SID = Guid.NewGuid().ToString(); - string errorMessage; - registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true); - - List activeRepos; - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(1); - this.VerifyRepo(activeRepos.SingleOrDefault(repo => repo.EnlistmentRoot.Equals(repo1Root)), owner1SID, expectedIsActive: true); - - // Deactivate repo - registry.TryDeactivateRepo(repo1Root, out errorMessage).ShouldEqual(true); - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(0); - - // Deactivate repo again (no-op) - registry.TryDeactivateRepo(repo1Root, out errorMessage).ShouldEqual(true); - registry.TryGetActiveRepos(out activeRepos, out errorMessage); - activeRepos.Count.ShouldEqual(0); - - // Repo should still be in the registry - Dictionary verifiableRegistry = registry.ReadRegistry(); - verifiableRegistry.Count.ShouldEqual(1); - this.VerifyRepo(verifiableRegistry[repo1Root], owner1SID, expectedIsActive: false); - - // Deactivate non-existent repo should fail - string nonExistantPath = Path.Combine("mock:", "test", "doesNotExist"); - registry.TryDeactivateRepo(nonExistantPath, out errorMessage).ShouldEqual(false); - errorMessage.ShouldContain("Attempted to deactivate non-existent repo"); - } - - [TestCase] - public void TraceStatus() - { - string dataLocation = Path.Combine("mock:", "registryDataFolder"); - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); - MockTracer tracer = new MockTracer(); - RepoRegistry registry = new RepoRegistry( - tracer, - fileSystem, - dataLocation, - this.mockRepoMounter.Object); - - string repo1Root = Path.Combine("mock:", "test", "repo1"); - string owner1SID = Guid.NewGuid().ToString(); - string repo2Root = Path.Combine("mock:", "test", "repo2"); - string owner2SID = Guid.NewGuid().ToString(); - string repo3Root = Path.Combine("mock:", "test", "repo3"); - string owner3SID = Guid.NewGuid().ToString(); - - string errorMessage; - registry.TryRegisterRepo(repo1Root, owner1SID, out errorMessage).ShouldEqual(true); - registry.TryRegisterRepo(repo2Root, owner2SID, out errorMessage).ShouldEqual(true); - registry.TryRegisterRepo(repo3Root, owner3SID, out errorMessage).ShouldEqual(true); - registry.TryDeactivateRepo(repo2Root, out errorMessage).ShouldEqual(true); - - registry.TraceStatus(); - - Dictionary repos = registry.ReadRegistry(); - repos.Count.ShouldEqual(3); - foreach (KeyValuePair kvp in repos) - { - tracer.RelatedInfoEvents.SingleOrDefault(message => message.Equals(kvp.Value.ToString())).ShouldNotBeNull(); - } - } - - private void VerifyRepo(RepoRegistration repo, string expectedOwnerSID, bool expectedIsActive) - { - repo.ShouldNotBeNull(); - repo.OwnerSID.ShouldEqual(expectedOwnerSID); - repo.IsActive.ShouldEqual(expectedIsActive); - } - } -} diff --git a/Scalar/CommandLine/CloneVerb.cs b/Scalar/CommandLine/CloneVerb.cs index 63c912ef01..b495c1f302 100644 --- a/Scalar/CommandLine/CloneVerb.cs +++ b/Scalar/CommandLine/CloneVerb.cs @@ -3,7 +3,7 @@ using Scalar.Common.FileSystem; using Scalar.Common.Git; using Scalar.Common.Http; -using Scalar.Common.NamedPipes; +using Scalar.Common.RepoRegistry; using Scalar.Common.Tracing; using System; using System.Diagnostics; @@ -280,8 +280,11 @@ private Result DoClone(string fullEnlistmentRootPathParameter, string normalized this.ConfigureWatchmanIntegration(); cloneResult = this.CheckoutRepo(); + } - this.RegisterWithService(); + if (cloneResult.Success) + { + cloneResult = this.TryRegisterRepo(); } return cloneResult; @@ -575,80 +578,37 @@ private Result CreateClone() return new Result(true); } - private void RegisterWithService() + private Result TryRegisterRepo() { - if (!this.Unattended) + if (this.Unattended) { - this.tracer.RelatedInfo($"{nameof(this.Execute)}: Registering with service"); - - string errorMessage = string.Empty; - if (this.ShowStatusWhileRunning( - () => { return this.RegisterRepoWithService(out errorMessage); }, - "Registering with service")) - { - this.tracer.RelatedInfo($"{nameof(this.Execute)}: Registered with service"); - } - else - { - this.Output.WriteLine(" WARNING: " + errorMessage); - this.tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register with service"); - } + this.tracer.RelatedInfo($"{nameof(this.Execute)}: Skipping repo registration (running Unattended)"); + return new Result(true); } - } - private bool RegisterRepoWithService(out string errorMessage) - { - errorMessage = string.Empty; - - NamedPipeMessages.RegisterRepoRequest request = new NamedPipeMessages.RegisterRepoRequest(); - request.EnlistmentRoot = this.enlistment.EnlistmentRoot; - - request.OwnerSID = ScalarPlatform.Instance.GetCurrentUser(); - - using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) + string errorMessage = string.Empty; + if (this.ShowStatusWhileRunning( + () => { return this.TryRegisterRepo(out errorMessage); }, + "Registering repo")) { - if (!client.Connect()) - { - errorMessage = "Unable to register repo because Scalar.Service is not responding."; - return false; - } + this.tracer.RelatedInfo($"{nameof(this.Execute)}: Registration succeeded"); + return new Result(true); + } - try - { - client.SendRequest(request.ToMessage()); - NamedPipeMessages.Message response = client.ReadResponse(); - if (response.Header == NamedPipeMessages.RegisterRepoRequest.Response.Header) - { - NamedPipeMessages.RegisterRepoRequest.Response message = NamedPipeMessages.RegisterRepoRequest.Response.FromMessage(response); + this.tracer.RelatedError($"{nameof(this.Execute)}: Failed to register repo: {errorMessage}"); + return new Result($"Failed to register repo: {errorMessage}"); + } - if (!string.IsNullOrEmpty(message.ErrorMessage)) - { - errorMessage = message.ErrorMessage; - return false; - } + private bool TryRegisterRepo(out string errorMessage) + { + string repoRegistryLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.RepoRegistry.RegistryDirectoryName); + ScalarRepoRegistry repoRegistry = new ScalarRepoRegistry( + this.tracer, + this.fileSystem, + repoRegistryLocation); - if (message.State != NamedPipeMessages.CompletionState.Success) - { - errorMessage = "Unable to register repo. " + errorMessage; - return false; - } - else - { - return true; - } - } - else - { - errorMessage = string.Format("Scalar.Service responded with unexpected message: {0}", response); - return false; - } - } - catch (BrokenPipeException e) - { - errorMessage = "Unable to communicate with Scalar.Service: " + e.ToString(); - return false; - } - } + this.tracer.RelatedInfo($"{nameof(this.Execute)}: Registering repo '{this.enlistment.EnlistmentRoot}'"); + return repoRegistry.TryRegisterRepo(this.enlistment.EnlistmentRoot, ScalarPlatform.Instance.GetCurrentUser(), out errorMessage); } private Result TryInitRepo() diff --git a/Scalar/CommandLine/ServiceVerb.cs b/Scalar/CommandLine/ServiceVerb.cs index 5336720828..06178d6823 100644 --- a/Scalar/CommandLine/ServiceVerb.cs +++ b/Scalar/CommandLine/ServiceVerb.cs @@ -1,9 +1,9 @@ using CommandLine; using Scalar.Common; -using Scalar.Common.NamedPipes; -using System; +using Scalar.Common.FileSystem; +using Scalar.Common.RepoRegistry; +using Scalar.Common.Tracing; using System.Collections.Generic; -using System.IO; using System.Linq; namespace Scalar.CommandLine @@ -37,73 +37,23 @@ public override void Execute() this.ReportErrorAndExit($"Error: You cannot specify multiple arguments. Run 'scalar {ServiceVerbName} --help' for details."); } - string errorMessage; - List repoList; - if (!this.TryGetRepoList(out repoList, out errorMessage)) + foreach (string repoRoot in this.GetRepoList()) { - this.ReportErrorAndExit("Error getting repo list: " + errorMessage); - } - - if (this.List) - { - foreach (string repoRoot in repoList) - { - this.Output.WriteLine(repoRoot); - } + this.Output.WriteLine(repoRoot); } } - private bool TryGetRepoList(out List repoList, out string errorMessage) + private IEnumerable GetRepoList() { - repoList = null; - errorMessage = string.Empty; - - NamedPipeMessages.GetActiveRepoListRequest request = new NamedPipeMessages.GetActiveRepoListRequest(); - - using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) + string repoRegistryLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.RepoRegistry.RegistryDirectoryName); + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "ServiceVerb")) { - if (!client.Connect()) - { - errorMessage = "Scalar.Service is not responding."; - return false; - } - - try - { - client.SendRequest(request.ToMessage()); - NamedPipeMessages.Message response = client.ReadResponse(); - if (response.Header == NamedPipeMessages.GetActiveRepoListRequest.Response.Header) - { - NamedPipeMessages.GetActiveRepoListRequest.Response message = NamedPipeMessages.GetActiveRepoListRequest.Response.FromMessage(response); - - if (!string.IsNullOrEmpty(message.ErrorMessage)) - { - errorMessage = message.ErrorMessage; - } - else - { - if (message.State != NamedPipeMessages.CompletionState.Success) - { - errorMessage = "Unable to retrieve repo list."; - } - else - { - repoList = message.RepoList; - return true; - } - } - } - else - { - errorMessage = string.Format("Scalar.Service responded with unexpected message: {0}", response); - } - } - catch (BrokenPipeException e) - { - errorMessage = "Unable to communicate with Scalar.Service: " + e.ToString(); - } + ScalarRepoRegistry repoRegistry = new ScalarRepoRegistry( + tracer, + new PhysicalFileSystem(), + repoRegistryLocation); - return false; + return repoRegistry.GetRegisteredRepos().Select(x => x.NormalizedRepoRoot); } } } diff --git a/Scripts/Mac/Scalar_Mount.sh b/Scripts/Mac/Scalar_Mount.sh deleted file mode 100755 index ab5a52a863..0000000000 --- a/Scripts/Mac/Scalar_Mount.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -. "$(dirname ${BASH_SOURCE[0]})/InitializeEnvironment.sh" - -CONFIGURATION=$1 -if [ -z $CONFIGURATION ]; then - CONFIGURATION=Debug -fi - -$SCALAR_OUTPUTDIR/Scalar/bin/$CONFIGURATION/netcoreapp3.0/osx-x64/publish/scalar mount ~/ScalarTest diff --git a/Scripts/Mac/Scalar_Unmount.sh b/Scripts/Mac/Scalar_Unmount.sh deleted file mode 100755 index da831874b8..0000000000 --- a/Scripts/Mac/Scalar_Unmount.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -. "$(dirname ${BASH_SOURCE[0]})/InitializeEnvironment.sh" - -CONFIGURATION=$1 -if [ -z $CONFIGURATION ]; then - CONFIGURATION=Debug -fi - -$SCALAR_OUTPUTDIR/Scalar/bin/$CONFIGURATION/netcoreapp3.0/osx-x64/publish/scalar unmount ~/ScalarTest